diff --git a/.gitignore b/.gitignore index 69ab2bd3..34f8c464 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ test/vagrant/site.mk test/cicd/site.mk doc/html +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f378b287..30a0b6aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: ``` diff --git a/apps/backend/backend_get.c b/apps/backend/backend_get.c index 6e80a7b5..7157a0a9 100644 --- a/apps/backend/backend_get.c +++ b/apps/backend/backend_get.c @@ -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) diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 53d980c1..45d1108f 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -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; } - diff --git a/apps/backend/clixon_backend_plugin.h b/apps/backend/clixon_backend_plugin.h index 5bcb8766..98daa126 100644 --- a/apps/backend/clixon_backend_plugin.h +++ b/apps/backend/clixon_backend_plugin.h @@ -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 *); diff --git a/apps/backend/clixon_backend_transaction.c b/apps/backend/clixon_backend_transaction.c index 48f25c27..40931a7f 100644 --- a/apps/backend/clixon_backend_transaction.c +++ b/apps/backend/clixon_backend_transaction.c @@ -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; +} diff --git a/apps/backend/clixon_backend_transaction.h b/apps/backend/clixon_backend_transaction.h index 8a4095ca..a14d7ccf 100644 --- a/apps/backend/clixon_backend_transaction.h +++ b/apps/backend/clixon_backend_transaction.h @@ -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_ */ diff --git a/example/main/example_backend.c b/example/main/example_backend.c index 7b952915..db5a816c 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -66,7 +66,7 @@ #include /* 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 -x + * Primarily for testing + * Start backend with -- -sS -x + */ +static char *_state_xpath = NULL; + /*! Read state file init on startup instead of on request * Primarily for testing * Start backend with -- -siS @@ -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, 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, 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; ixlen) upper = xlen; - *remaining = xlen - upper; + remaining = xlen - upper; + pagination_remaining_set(pd, remaining); } break; } @@ -564,7 +649,7 @@ example_statefile(clicon_handle h, xml_free(xt); if (xvec) free(xvec); - return retval; + return retval; } /*! Lock databse status has changed status @@ -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 */ _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 diff --git a/example/main/example_backend_nacm.c b/example/main/example_backend_nacm.c index 655e4d1b..b9e773c2 100644 --- a/example/main/example_backend_nacm.c +++ b/example/main/example_backend_nacm.c @@ -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 */ diff --git a/lib/clixon/clixon.h.in b/lib/clixon/clixon.h.in index 028761d1..c5aac2d6 100644 --- a/lib/clixon/clixon.h.in +++ b/lib/clixon/clixon.h.in @@ -108,6 +108,7 @@ extern "C" { #include #include #include +#include /* * Global variables generated by Makefile diff --git a/lib/clixon/clixon_data.h b/lib/clixon/clixon_data.h index e60b0542..e4bed3ea 100644 --- a/lib/clixon/clixon_data.h +++ b/lib/clixon/clixon_data.h @@ -61,10 +61,15 @@ typedef struct { /* * Prototypes */ +/* Generic clixon data API the form = where 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); diff --git a/lib/clixon/clixon_dispatcher.h b/lib/clixon/clixon_dispatcher.h new file mode 100644 index 00000000..fe4350ec --- /dev/null +++ b/lib/clixon/clixon_dispatcher.h @@ -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 */ diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index 95b4029e..338b21ef 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -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 diff --git a/lib/src/Makefile.in b/lib/src/Makefile.in index 444120a7..55a48145 100644 --- a/lib/src/Makefile.in +++ b/lib/src/Makefile.in @@ -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 \ diff --git a/lib/src/clixon_data.c b/lib/src/clixon_data.c index 13e6b35a..c011f81f 100644 --- a/lib/src/clixon_data.c +++ b/lib/src/clixon_data.c @@ -134,6 +134,66 @@ clicon_data_del(clicon_handle h, return clicon_hash_del(cdat, (char*)name); } +/*! Get generic clixon data on the form = where 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 = where 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 = where 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 diff --git a/lib/src/clixon_dispatcher.c b/lib/src/clixon_dispatcher.c new file mode 100644 index 00000000..18b13f66 --- /dev/null +++ b/lib/src/clixon_dispatcher.c @@ -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 +#include +#include +#include +#include + +#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; +} diff --git a/test/example_social.sh b/test/example_social.sh index 7daea9ab..9fc1b977 100755 --- a/test/example_social.sh +++ b/test/example_social.sh @@ -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 < $fexample module example-social { @@ -214,6 +215,10 @@ cat < $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; diff --git a/test/test_augment_state.sh b/test/test_augment_state.sh index eb8576a1..9c0da067 100755 --- a/test/test_augment_state.sh +++ b/test/test_augment_state.sh @@ -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 diff --git a/test/test_pagination_state.sh b/test/test_pagination_state.sh index fcefc593..ef1fef37 100755 --- a/test/test_pagination_state.sh +++ b/test/test_pagination_state.sh @@ -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 < $cfg @@ -51,8 +53,31 @@ cat < $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< $fstate + + + alice + + 3 + 4 + 5 + 6 + 7 + 8 + + + + bob + + 13 + 14 + 15 + 16 + 17 + 18 + + + EOF # Append generated state data to $fstate file @@ -68,44 +93,82 @@ for (( i=0; i<$perfnr; i++ )); do echo " POST" >> $fstate echo " " >> $fstate done + echo -n "" >> $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 "wait backend" + wait_backend +} - new "start backend -s init -f $cfg -- -siS $fstate" - start_backend -s init -f $cfg -- -siS $fstate -fi +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 +} -new "wait backend" -wait_backend +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 "$DEFAULTHELLOtrue010]]>]]>" "^alicepublic345678]]>]]>$" + +# negative +new "NETCONF get container, expect fail" +expecteof "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLOtrue010]]>]]>" "^applicationinvalid-valueerrorlist-pagination is enabled but target is not list or leaf-list]]>]]>$" + +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 "$DEFAULTHELLOtrue010]]>]]>" "^alicepublic345678bobpublic13141516]]>]]>$" + +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