From 4169bd8d30cc36a1632566c29ad14d654445b68a Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 8 Apr 2017 20:39:04 +0200 Subject: [PATCH 01/24] Ongoing: xmldb datastore plugin framework --- CHANGELOG | 2 + Makefile.in | 2 +- apps/backend/backend_main.c | 17 +- configure | 4 +- configure.ac | 2 + datastore/Makefile.in | 78 + datastore/keyvalue/Makefile.in | 100 + datastore/keyvalue/clixon_keyvalue.c | 1746 +++++++++++++++ datastore/keyvalue/clixon_keyvalue.h | 56 + {lib/src => datastore/keyvalue}/clixon_qdb.c | 0 {lib/src => datastore/keyvalue}/clixon_qdb.h | 0 lib/clixon/clixon_options.h | 1 + lib/clixon/clixon_xml_db.h | 75 +- lib/clixon/clixon_xml_map.h | 3 + lib/src/Makefile.in | 7 +- lib/src/clixon_options.c | 10 + lib/src/clixon_xml_db.c | 2120 ++---------------- lib/src/clixon_xml_map.c | 289 ++- 18 files changed, 2588 insertions(+), 1924 deletions(-) create mode 100644 datastore/Makefile.in create mode 100644 datastore/keyvalue/Makefile.in create mode 100644 datastore/keyvalue/clixon_keyvalue.c create mode 100644 datastore/keyvalue/clixon_keyvalue.h rename {lib/src => datastore/keyvalue}/clixon_qdb.c (100%) rename {lib/src => datastore/keyvalue}/clixon_qdb.h (100%) diff --git a/CHANGELOG b/CHANGELOG index 3731f1f6..450f5b6c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,6 +29,8 @@ # # ***** END LICENSE BLOCK ***** +- Ongoiing: xmldb datastore plugin framework + - cli_copy_config added as generic cli command - cli_show_config added as generic cli command Replace all show_confv*() and show_conf*() with cli_show_config() diff --git a/Makefile.in b/Makefile.in index f858d94c..3447442c 100644 --- a/Makefile.in +++ b/Makefile.in @@ -52,7 +52,7 @@ LIBS = @LIBS@ INCLUDES = -I. -I@srcdir@ @INCLUDES@ SHELL = /bin/sh -SUBDIRS = lib apps include etc +SUBDIRS = lib apps include etc datastore .PHONY: doc all clean depend $(SUBDIRS) install loc TAGS .config.status docker diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 6ee74fc9..fd23e266 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -73,7 +73,7 @@ #include "backend_handle.h" /* Command line options to be passed to getopt(3) */ -#define BACKEND_OPTS "hD:f:d:Fzu:P:1IRCc:rg:pty:" +#define BACKEND_OPTS "hD:f:d:Fzu:P:1IRCc:rg:pty:x:" /*! Terminate. Cannot use h after this */ static int @@ -143,7 +143,8 @@ usage(char *argv0, clicon_handle h) " -p \t\tPrint database yang specification\n" " -t \t\tPrint alternate spec translation (eg if YANG print KEY, if KEY print YANG)\n" " -g \tClient membership required to this group (default: %s)\n" - "\t-y \tOverride yang spec file (dont include .yang suffix)\n", + "\t-y \tOverride yang spec file (dont include .yang suffix)\n" + "\t-x \tXMLDB plugin\n", argv0, plgdir ? plgdir : "none", confsock ? confsock : "none", @@ -314,6 +315,7 @@ main(int argc, char **argv) char *pidfile; char *sock; int sockfamily; + char *xmldb_plugin; /* In the startup, logs to stderr & syslog and debug flag set later */ @@ -437,6 +439,10 @@ main(int argc, char **argv) clicon_option_str_set(h, "CLICON_YANG_DIR", strdup(dir)); break; } + case 'x' :{ /* xmldb plugin */ + clicon_option_str_set(h, "CLICON_XMLDB_PLUGIN", optarg); + break; + } default: usage(argv[0], h); break; @@ -502,6 +508,13 @@ main(int argc, char **argv) return -1; } + if ((xmldb_plugin = clicon_xmldb_plugin(h)) == NULL){ + clicon_log(LOG_ERR, "No xmldb plugin given (specify option CLICON_XMLDB_PLUGIN).\n"); + goto done; + } + if (xmldb_plugin_load(xmldb_plugin) < 0) + goto done; + /* Parse db spec file */ if (yang_spec_main(h, stdout, printspec) < 0) goto done; diff --git a/configure b/configure index 3e5cf739..e7a3ac5f 100755 --- a/configure +++ b/configure @@ -4315,7 +4315,7 @@ fi -ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile doc/Makefile" +ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile doc/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -5030,6 +5030,8 @@ do "docker/backend/Dockerfile") CONFIG_FILES="$CONFIG_FILES docker/backend/Dockerfile" ;; "docker/netconf/Makefile") CONFIG_FILES="$CONFIG_FILES docker/netconf/Makefile" ;; "docker/netconf/Dockerfile") CONFIG_FILES="$CONFIG_FILES docker/netconf/Dockerfile" ;; + "datastore/Makefile") CONFIG_FILES="$CONFIG_FILES datastore/Makefile" ;; + "datastore/keyvalue/Makefile") CONFIG_FILES="$CONFIG_FILES datastore/keyvalue/Makefile" ;; "doc/Makefile") CONFIG_FILES="$CONFIG_FILES doc/Makefile" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; diff --git a/configure.ac b/configure.ac index 9b394fa3..c156443f 100644 --- a/configure.ac +++ b/configure.ac @@ -201,6 +201,8 @@ AC_OUTPUT(Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile + datastore/Makefile + datastore/keyvalue/Makefile doc/Makefile ) diff --git a/datastore/Makefile.in b/datastore/Makefile.in new file mode 100644 index 00000000..7ad7112f --- /dev/null +++ b/datastore/Makefile.in @@ -0,0 +1,78 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# +# Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren +# +# This file is part of CLIXON +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Alternatively, the contents of this file may be used under the terms of +# the GNU General Public License Version 3 or later (the "GPL"), +# in which case the provisions of the GPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of the GPL, and not to allow others to +# use your version of this file under the terms of Apache License version 2, +# indicate your decision by deleting the provisions above and replace them with +# the notice and other provisions required by the GPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the Apache License version 2 or the GPL. +# +# ***** END LICENSE BLOCK ***** +# +VPATH = @srcdir@ +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +CC = @CC@ +CFLAGS = @CFLAGS@ +LDFLAGS = @LDFLAGS@ +LIBS = @LIBS@ + +SHELL = /bin/sh + +SUBDIRS = keyvalue + +.PHONY: all clean depend install $(SUBDIRS) + +all: $(SUBDIRS) + +depend: + for i in $(SUBDIRS); \ + do (cd $$i; $(MAKE) $(MFLAGS) $@); done + +$(SUBDIRS): + (cd $@; $(MAKE) $(MFLAGS) all) + +install-include: + for i in $(SUBDIRS); \ + do (cd $$i ; $(MAKE) $(MFLAGS) $@); done; + +install: + for i in $(SUBDIRS); \ + do (cd $$i; $(MAKE) $(MFLAGS) $@); done + +uninstall: + for i in $(SUBDIRS); \ + do (cd $$i; $(MAKE) $(MFLAGS) $@); done + +clean: + for i in $(SUBDIRS); \ + do (cd $$i; $(MAKE) $(MFLAGS) $@); done + +distclean: clean + rm -f Makefile *~ .depend + for i in $(SUBDIRS); \ + do (cd $$i; $(MAKE) $(MFLAGS) $@); done + +tags: + find $(srcdir) -name '*.[chyl]' -print | etags - diff --git a/datastore/keyvalue/Makefile.in b/datastore/keyvalue/Makefile.in new file mode 100644 index 00000000..7c79e7f5 --- /dev/null +++ b/datastore/keyvalue/Makefile.in @@ -0,0 +1,100 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# +# Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren +# +# This file is part of CLIXON +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Alternatively, the contents of this file may be used under the terms of +# the GNU General Public License Version 3 or later (the "GPL"), +# in which case the provisions of the GPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of the GPL, and not to allow others to +# use your version of this file under the terms of Apache License version 2, +# indicate your decision by deleting the provisions above and replace them with +# the notice and other provisions required by the GPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the Apache License version 2 or the GPL. +# +# ***** END LICENSE BLOCK ***** +# +prefix = @prefix@ +datarootdir = @datarootdir@ +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +libdir = @libdir@ +dbdir = @prefix@/db +mandir = @mandir@ +libexecdir = @libexecdir@ +localstatedir = @localstatedir@ +sysconfdir = @sysconfdir@ + +VPATH = @srcdir@ +CC = @CC@ +CFLAGS = @CFLAGS@ -rdynamic -fPIC +LDFLAGS = @LDFLAGS@ +LIBS = @LIBS@ +DATASTORE = keyvalue +CPPFLAGS = @CPPFLAGS@ + +INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$(top_srcdir) + +PLUGIN = $(DATASTORE).so + +SRC = clixon_keyvalue.c clixon_qdb.c + +OBJS = $(SRC:.c=.o) + +all: $(PLUGIN) + +-include $(DESTDIR)$(datarootdir)/clixon/clixon.mk + +#grideye_sysinfo.so.1: grideye_sysinfo.c +# $(CC) $(CFLAGS) -shared -o $@ -lc $^ -lm + +$(PLUGIN): $(SRC) + $(CC) $(CPPFLAGS) $(INCLUDES) $(CFLAGS) -shared -o $@ -lc $^ $(LIBS) + +clean: + rm -f $(PLUGIN) $(OBJS) *.core + +distclean: clean + rm -f Makefile *~ .depend + +.SUFFIXES: +.SUFFIXES: .c .o + +.c.o: $(SRC) + $(CC) $(INCLUDES) $(CPPFLAGS) $(CFLAGS) -c $< + +install: $(PLUGIN) + install -d $(DESTDIR)$(clixon_LIBDIR)/xmldb + install $(PLUGIN) $(DESTDIR)$(clixon_LIBDIR)/xmldb; + +install-include: + +uninstall: + rm -rf $(DESTDIR)$(clixon_LIBDIR)/xmldb/$(PLUGIN); + +TAGS: + find . -name '*.[chyl]' -print | etags - + +depend: + $(CC) $(DEPENDFLAGS) @DEFS@ $(INCLUDES) $(CFLAGS) -MM $(SRC) > .depend + +#include .depend + diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c new file mode 100644 index 00000000..faaf1242 --- /dev/null +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -0,0 +1,1746 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + */ +/* + * An xml database consists of key-value pairs for xml-trees. + * Each node in an xml-tree has a key and an optional value. + * The key (xmlkey) is constructed from the xml node name concatenated + * with its ancestors and any eventual list keys. + * A xmlkeyfmt is a help-structure used when accessing the XML database. + * It consists of an xmlkey but with the key fields replaced with wild-chars(%s) + * Example: /aaa/bbb/%s/%s/ccc + * Such an xmlkeyfmt can be obtained from a yang-statement by following + * its ancestors to the root module. If one of the ancestors is a list, + * a wildchar (%s) is inserted for each key. + * These xmlkeyfmt keys are saved and used in cli callbacks such as when + * modifying syntax (eg cli_merge/cli_delete) or when completing for sub-symbols + * In this case, the variables are set and the wildcards can be instantiated. + * An xml tree can then be formed that can be used to the xmldb_get() or + * xmldb_put() functions. + * The relations between the functions and formats are as follows: + * + * +-----------------+ +-----------------+ + * | yang-stmt | yang2xmlkeyfmt | xmlkeyfmt | xmlkeyfmt2xpath + * | list aa,leaf k | ----------------->| /aa=%s |----------------> + * +-----------------+ +-----------------+ + * | + * | xmlkeyfmt2key + * | k=17 + * v + * +-------------------+ +-----------------+ + * | xml-tree/cxobj | xmlkey2xml | xmlkey RFC3986| + * | 17| <------------- | /aa=17 | + * +-------------------+ +-----------------+ + * + * Alternative for xmlkeyfmt would be eg: + * RESTCONF: /interfaces/interface=%s/ipv4/address/ip=%s (used) + * XPATH: /interfaces/interface[name=%s]/ipv4/address/[ip=%s] + * + * Paths through the code (for coverage) + * cli_callback_generate +----------------+ + * cli_expand_var_generate | yang2xmlkeyfmt | + * yang -------------> | | + * +----------------+ + * xmldb_get_tree + * - compare_dbs + * - netconf + * - validate + * - from_client_save + * + * xmldb_get_vec + * - restconf + * - expand_dbvar + * - show_conf_xpath + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include "clixon_qdb.h" +#include "clixon_keyvalue.h" + +/*! Database locking for candidate and running non-persistent + * Store an integer for running and candidate containing + * the session-id of the client holding the lock. + */ +static int _running_locked = 0; +static int _candidate_locked = 0; +static int _startup_locked = 0; + +/*! Lock database + * @param[in] db Database name + * @param[in] pid process/session-id + */ +static int +db_lock(char *db, + int pid) +{ + if (strcmp("running", db) == 0) + _running_locked = pid; + else if (strcmp("candidate", db) == 0) + _candidate_locked = pid; + else if (strcmp("startup", db) == 0) + _startup_locked = pid; + clicon_debug(1, "%s: locked by %u", db, pid); + return 0; +} + +/*! Unlock database + * @param[in] db Database name + */ +static int +db_unlock(char *db) +{ + if (strcmp("running", db) == 0) + _running_locked = 0; + else if (strcmp("candidate", db) == 0) + _candidate_locked = 0; + else if (strcmp("startup", db) == 0) + _startup_locked = 0; + return 0; +} + +/*! Unlock all databases locked by pid (eg process dies) + * @param[in] pid process/session-id + */ +static int +db_unlock_all(int pid) +{ + if (_running_locked == pid) + _running_locked = 0; + if (_candidate_locked == pid) + _candidate_locked = 0; + if (_startup_locked == pid) + _startup_locked = 0; + return 0; +} + +/*! returns id of locker + * @retval 0 Not locked + * @retval >0 Id of locker + */ +static int +db_islocked(char *db) +{ + if (strcmp("running", db) == 0) + return (_running_locked); + else if (strcmp("candidate", db) == 0) + return(_candidate_locked); + else if (strcmp("startup", db) == 0) + return(_startup_locked); + return 0; +} + +/*! Translate from symbolic database name to actual filename in file-system + * @param[in] h Clicon handle + * @param[in] db Symbolic database name, eg "candidate", "running" + * @param[out] filename Filename. Unallocate after use with free() + * @retval 0 OK + * @retval -1 Error + * @note Could need a way to extend which databases exists, eg to register new. + * The currently allowed databases are: + * candidate, tmp, running, result + * The filename reside in CLICON_XMLDB_DIR option + */ +static int +db2file(clicon_handle h, + char *db, + char **filename) +{ + int retval = -1; + cbuf *cb; + char *dir; + + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if ((dir = clicon_xmldb_dir(h)) == NULL){ + clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set"); + goto done; + } + if (strcmp(db, "running") != 0 && + strcmp(db, "candidate") != 0 && + strcmp(db, "startup") != 0 && + strcmp(db, "tmp") != 0){ + clicon_err(OE_XML, 0, "Unexpected database: %s", db); + goto done; + } + cprintf(cb, "%s/%s_db", dir, db); + if ((*filename = strdup4(cbuf_get(cb))) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + +/*! Help function to append key values from an xml list to a cbuf + * Example, a yang node x with keys a and b results in "x/a/b" + */ +static int +append_listkeys(cbuf *ckey, + cxobj *xt, + yang_stmt *ys) +{ + int retval = -1; + yang_stmt *ykey; + cxobj *xkey; + cg_var *cvi; + cvec *cvk = NULL; /* vector of index keys */ + char *keyname; + char *bodyenc; + int i=0; + + if ((ykey = yang_find((yang_node*)ys, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, ys->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + cvi = NULL; + /* Iterate over individual keys */ + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + if ((xkey = xml_find(xt, keyname)) == NULL){ + clicon_err(OE_XML, errno, "XML list node \"%s\" does not have key \"%s\" child", + xml_name(xt), keyname); + goto done; + } + if ((bodyenc = curl_easy_escape(NULL, xml_body(xkey), 0)) == NULL){ + clicon_err(OE_UNIX, errno, "curl_easy_escape"); + goto done; + } + if (i++) + cprintf(ckey, ","); + else + cprintf(ckey, "="); + cprintf(ckey, "%s", bodyenc); + free(bodyenc); bodyenc = NULL; + } + retval = 0; + done: + if (cvk) + cvec_free(cvk); + return retval; +} + +/*! Help function to create xml key values + * @param[in,out] x Parent + * @param[in] ykey + * @param[in] arg + * @param[in] keyname yang key name + */ +static int +create_keyvalues(cxobj *x, + yang_stmt *ykey, + char *arg, + char *keyname) +{ + int retval = -1; + cxobj *xb; + + /* Check if key node exists */ + if ((xb = xml_new_spec(keyname, x, ykey)) == NULL) + goto done; + if ((xb = xml_new("body", xb)) == NULL) + goto done; + xml_type_set(xb, CX_BODY); + xml_value_set(xb, arg); + retval = 0; + done: + return retval; +} + +/*! Prune everything that has not been marked + * @param[in] xt XML tree with some node marked + * @param[out] upmark Set if a child (recursively) has marked set. + * The function removes all branches that does not contain a marked child + * XXX: maybe key leafs should not be purged if list is not purged? + * XXX: consider move to clicon_xml + */ +static int +xml_tree_prune_unmarked(cxobj *xt, + int *upmark) +{ + int retval = -1; + int submark; + int mark; + cxobj *x; + cxobj *xprev; + + mark = 0; + x = NULL; + xprev = x = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + if (xml_flag(x, XML_FLAG_MARK)){ + mark++; + xprev = x; + continue; /* mark and stop here */ + } + if (xml_tree_prune_unmarked(x, &submark) < 0) + goto done; + if (submark) + mark++; + else{ /* Safe with xml_child_each if last */ + if (xml_purge(x) < 0) + goto done; + x = xprev; + } + xprev = x; + } + retval = 0; + done: + if (upmark) + *upmark = mark; + return retval; +} + +/*! + * @param[in] xk xmlkey + * @param[out] xt XML tree as result + * XXX cannot handle top-level list + */ +static int +get(char *dbname, + yang_spec *ys, + char *xk, + char *val, + cxobj *xt) +{ + int retval = -1; + char **vec = NULL; + int nvec; + char **valvec = NULL; + int nvalvec; + int i; + int j; + char *name; + char *restval; + yang_stmt *y; + cxobj *x; + cxobj *xc; + cxobj *xb; + yang_stmt *ykey; + cg_var *cvi; + cvec *cvk = NULL; /* vector of index keys */ + char *keyname; + char *arg; + char *argdec; + cbuf *cb; + + // clicon_debug(1, "%s xkey:%s val:%s", __FUNCTION__, xk, val); + x = xt; + if (xk == NULL || *xk!='/'){ + clicon_err(OE_DB, 0, "Invalid key: %s", xk); + goto done; + } + if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) + goto done; + /* Element 0 is NULL '/', + Element 1 is top symbol and needs to find subs in all modules: + spec->module->syntaxnode + */ + if (nvec < 2){ + clicon_err(OE_XML, 0, "Malformed key: %s", xk); + goto done; + } + i = 1; + while (i name:x restval=1,2 */ + if ((restval = index(name, '=')) != NULL){ + *restval = '\0'; + restval++; + } + if (i == 1){ /* spec->module->node */ + if ((y = yang_find_topnode(ys, name)) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", name); + goto done; + } + } + else + if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", name); + goto done; + } + switch (y->ys_keyword){ + case Y_LEAF_LIST: + /* + * If xml element is a leaf-list, then the next element is expected to + * be a value + */ + if ((argdec = curl_easy_unescape(NULL, restval, 0, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "curl_easy_escape"); + goto done; + } + if ((xc = xml_find(x, name))==NULL || + (xb = xml_find(xc, argdec))==NULL){ + if ((xc = xml_new_spec(name, x, y)) == NULL) + goto done; + /* Assume body is created at end of function */ + } + free(argdec); + argdec = NULL; + break; + case Y_LIST: + /* + * If xml element is a list, then the next element(s) is expected to be + * a key value. Check if this key value is already in the xml tree, + * otherwise create it. + */ + if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, y->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + cvi = NULL; + /* Iterate over individual yang keys */ + cprintf(cb, "%s", name); + if (valvec) + free(valvec); + if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) + goto done; + if (cvec_len(cvk)!=nvalvec){ + retval = 0; + goto done; + } + j = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL){ + if (j>=nvalvec) + break; + arg = valvec[j++]; + if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "curl_easy_escape"); + goto done; + } + cprintf(cb, "[%s=%s]", cv_string_get(cvi), argdec); + free(argdec); + argdec=NULL; + } + if ((xc = xpath_first(x, cbuf_get(cb))) == NULL){ + if ((xc = xml_new_spec(name, x, y)) == NULL) + goto done; + cvi = NULL; + // i -= cvec_len(cvk); + /* Iterate over individual yang keys */ + j=0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + if (j>=nvalvec) + break; + arg = valvec[j++]; + keyname = cv_string_get(cvi); + if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ + clicon_err(OE_UNIX, errno, "curl_easy_escape"); + goto done; + } + if (create_keyvalues(xc, + ykey, + argdec, + keyname) < 0) + goto done; + free(argdec); argdec = NULL; + } /* while */ + } + if (cb){ + cbuf_free(cb); + cb = NULL; + } + if (cvk){ + cvec_free(cvk); + cvk = NULL; + } + break; + case Y_LEAF: + case Y_CONTAINER: + default: + if ((xc = xml_find(x, name))==NULL) + if ((xc = xml_new_spec(name, x, y)) == NULL) + goto done; + break; + } /* switch */ + x = xc; + i++; + } + if (val && xml_body(x)==NULL){ + if ((x = xml_new("body", x)) == NULL) + goto done; + xml_type_set(x, CX_BODY); + xml_value_set(x, val); + } + if(debug>1){ + fprintf(stderr, "%s %s\n", __FUNCTION__, xk); + clicon_xml2file(stderr, xt, 0, 1); + } + retval = 0; + done: + if (vec) + free(vec); + if (valvec) + free(valvec); + if (cvk) + cvec_free(cvk); + return retval; +} + +/*! Sanitize an xml tree: xml node has matching yang_stmt pointer + */ +static int +xml_sanity(cxobj *x, + void *arg) +{ + int retval = -1; + yang_stmt *ys; + + ys = (yang_stmt*)xml_spec(x); + if (ys==NULL){ + clicon_err(OE_XML, 0, "No spec for xml node %s", xml_name(x)); + goto done; + } + if (strstr(ys->ys_argument, xml_name(x))==NULL){ + clicon_err(OE_XML, 0, "xml node name '%s' does not match yang spec arg '%s'", + xml_name(x), ys->ys_argument); + goto done; + } + retval = 0; + done: + return retval; +} + +/*! Add default values (if not set) + */ +static int +xml_default(cxobj *x, + void *arg) +{ + int retval = -1; + yang_stmt *ys; + yang_stmt *y; + int i; + cxobj *xc; + cxobj *xb; + char *str; + + ys = (yang_stmt*)xml_spec(x); + /* Check leaf defaults */ + if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){ + for (i=0; iys_len; i++){ + y = ys->ys_stmt[i]; + if (y->ys_keyword != Y_LEAF) + continue; + assert(y->ys_cv); + if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */ + if (!xml_find(x, y->ys_argument)){ + if ((xc = xml_new_spec(y->ys_argument, x, y)) == NULL) + goto done; + if ((xb = xml_new("body", xc)) == NULL) + goto done; + xml_type_set(xb, CX_BODY); + if ((str = cv2str_dup(y->ys_cv)) == NULL){ + clicon_err(OE_UNIX, errno, "cv2str_dup"); + goto done; + } + if (xml_value_set(xb, str) < 0) + goto done; + free(str); + } + } + } + } + retval = 0; + done: + return retval; +} + +/*! Order XML children according to YANG + */ +static int +xml_order(cxobj *x, + void *arg) +{ + int retval = -1; + yang_stmt *y; + yang_stmt *yc; + int i; + int j0; + int j; + cxobj *xc; + cxobj *xj; + char *yname; /* yang child name */ + char *xname; /* xml child name */ + + y = (yang_stmt*)xml_spec(x); + j0 = 0; + /* Go through xml children and ensure they are same order as yspec children */ + for (i=0; iys_len; i++){ + yc = y->ys_stmt[i]; + if (!yang_is_syntax(yc)) + continue; + yname = yc->ys_argument; + /* First go thru xml children with same name */ + for (;j01) + clicon_xml2file(stderr, xt, 0, 1); + *xtop = xt; + retval = 0; + done: + if (dbname) + free(dbname); + if (xvec) + free(xvec); + unchunk_group(__FUNCTION__); + return retval; + +} + +/*! Add data to database internal recursive function + * @param[in] dbname Name of database to search in (filename incl dir path) + * @param[in] xt xml-node. + * @param[in] ys Yang statement corresponding to xml-node + * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc + * @param[in] xkey0 aggregated xmlkey + * @retval 0 OK + * @retval -1 Error + * @note XXX op only supports merge + */ +static int +put(char *dbname, + cxobj *xt, + yang_stmt *ys, + enum operation_type op, + const char *xk0) +{ + int retval = -1; + cxobj *x = NULL; + char *xk; + cbuf *cbxk = NULL; + char *body; + yang_stmt *y; + int exists; + char *bodyenc=NULL; + char *opstr; + + clicon_debug(1, "%s xk0:%s ys:%s", __FUNCTION__, xk0, ys->ys_argument); + if (debug){ + xml_print(stderr, xt); + // yang_print(stderr, (yang_node*)ys, 0); + } + if ((opstr = xml_find_value(xt, "operation")) != NULL) + if (xml_operation(opstr, &op) < 0) + goto done; + body = xml_body(xt); + if ((cbxk = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(cbxk, "%s/%s", xk0, xml_name(xt)); + switch (ys->ys_keyword){ + case Y_LIST: /* Note: can have many keys */ + if (append_listkeys(cbxk, xt, ys) < 0) + goto done; + break; + case Y_LEAF_LIST: + if ((bodyenc = curl_easy_escape(NULL, body, 0)) == NULL){ + clicon_err(OE_UNIX, errno, "curl_easy_escape"); + goto done; + } + cprintf(cbxk, "=%s", bodyenc); + break; + default: + break; + } + xk = cbuf_get(cbxk); + // fprintf(stderr, "%s %s\n", key, body?body:""); + /* Write to database, key and a vector of variables */ + switch (op){ + case OP_CREATE: + if ((exists = db_exists(dbname, xk)) < 0) + goto done; + if (exists == 1){ + clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk); + goto done; + } + case OP_MERGE: + case OP_REPLACE: + if (db_set(dbname, xk, body?body:NULL, body?strlen(body)+1:0) < 0) + goto done; + break; + case OP_DELETE: + if ((exists = db_exists(dbname, xk)) < 0) + goto done; + if (exists == 0){ + clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); + goto done; + } + case OP_REMOVE: + if (db_del(dbname, xk) < 0) + goto done; + break; + case OP_NONE: + break; + } + /* For every node, create a key with values */ + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ + if ((y = yang_find_syntax((yang_node*)ys, xml_name(x))) == NULL){ + clicon_err(OE_UNIX, 0, "No yang node found: %s", xml_name(x)); + goto done; + } + if (put(dbname, x, y, op, xk) < 0) + goto done; + } + retval = 0; + done: + if (cbxk) + cbuf_free(cbxk); + if (bodyenc) + free(bodyenc); + return retval; +} + +/*! Modify database provided an XML database key and an operation + * @param[in] h CLICON handle + * @param[in] db Database name + * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc + * @param[in] xk XML Key, eg /aa/bb=17/name + * @param[in] val Key value, eg "17" + + * @retval 0 OK + * @retval -1 Error + * @code + * if (xmldb_put_xkey(h, db, OP_MERGE, "/aa/bb=17/name", "17") < 0) + * err; + * @endcode + * @see xmldb_put with xml-tree, no path + */ +static int +xmldb_put_xkey(clicon_handle h, + char *db, + enum operation_type op, + char *xk, + char *val) + +{ + int retval = -1; + cxobj *x = NULL; + yang_stmt *y = NULL; + yang_stmt *ykey; + char **vec = NULL; + int nvec; + char **valvec = NULL; + int nvalvec; + int i; + int j; + char *name; + char *restval; + cg_var *cvi; + cvec *cvk = NULL; /* vector of index keys */ + char *val2 = NULL; + cbuf *ckey=NULL; /* partial keys */ + cbuf *csubkey=NULL; /* partial keys */ + cbuf *crx=NULL; /* partial keys */ + char *keyname; + int exists; + int npairs; + struct db_pair *pairs; + yang_spec *yspec; + char *filename = NULL; + + yspec = clicon_dbspec_yang(h); + if (db2file(h, db, &filename) < 0) + goto done; + if (xk == NULL || *xk!='/'){ + clicon_err(OE_DB, 0, "Invalid key: %s", xk); + goto done; + } + if ((ckey = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((csubkey = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) + goto done; + if (nvec < 2){ + clicon_err(OE_XML, 0, "Malformed key: %s", xk); + goto done; + } + i = 1; + while (i name:x restval=1,2 */ + if ((restval = index(name, '=')) != NULL){ + *restval = '\0'; + restval++; + } + if (i==1){ + if (strlen(name)==0 && (op==OP_DELETE || op == OP_REMOVE)){ + /* Special handling of "/" */ + cprintf(ckey, "/"); + break; + } + else + if ((y = yang_find_topnode(yspec, name)) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", x?xml_name(x):""); + goto done; + } + } + else + if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", name); + goto done; + } + if ((op==OP_DELETE || op == OP_REMOVE) && + y->ys_keyword == Y_LEAF && + y->ys_parent->yn_keyword == Y_LIST && + yang_key_match(y->ys_parent, y->ys_argument)) + /* Special rule if key, dont write last key-name, rm whole*/; + else + cprintf(ckey, "/%s", name); + i++; + switch (y->ys_keyword){ + case Y_LEAF_LIST: + if (restval==NULL){ + clicon_err(OE_XML, 0, "malformed key, expected '='"); + goto done; + } + cprintf(ckey, "=%s", restval); + break; + case Y_LIST: + if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, y->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + if (restval==NULL){ + clicon_err(OE_XML, 0, "malformed key, expected '='"); + goto done; + } + if (valvec) + free(valvec); + if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) + goto done; + + if (cvec_len(cvk) != nvalvec){ + clicon_err(OE_XML, errno, "List %s key length mismatch", name); + goto done; + } + cvi = NULL; + /* Iterate over individual yang keys */ + j = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + if (j) + cprintf(ckey, ","); + else + cprintf(ckey, "="); + val2 = valvec[j++]; + + cprintf(ckey, "%s", val2); + cbuf_reset(csubkey); + cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); + if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) + if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0) + goto done; + } + if (cvk){ + cvec_free(cvk); + cvk = NULL; + } + break; + default: + if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) + if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0) + goto done; + break; + } + } + xk = cbuf_get(ckey); + /* final key */ + switch (op){ + case OP_CREATE: + if ((exists = db_exists(filename, xk)) < 0) + goto done; + if (exists == 1){ + clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk); + goto done; + } + case OP_MERGE: + case OP_REPLACE: + if (y->ys_keyword == Y_LEAF || y->ys_keyword == Y_LEAF_LIST){ + if (db_set(filename, xk, val, val?strlen(val)+1:0) < 0) + goto done; + } + else + if (db_set(filename, xk, NULL, 0) < 0) + goto done; + break; + case OP_DELETE: + if ((exists = db_exists(filename, xk)) < 0) + goto done; + if (exists == 0){ + clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); + goto done; + } + case OP_REMOVE: + /* Read in complete database (this can be optimized) */ + if ((crx = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(crx, "^%s.*$", xk); + if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0) + goto done; + for (i = 0; i < npairs; i++) { + if (db_del(filename, pairs[i].dp_key) < 0) + goto done; + } + break; + default: + break; + } + retval = 0; + done: + if (filename) + free(filename); + if (ckey) + cbuf_free(ckey); + if (csubkey) + cbuf_free(csubkey); + if (crx) + cbuf_free(crx); + if (cvk) + cvec_free(cvk); + if (vec) + free(vec); + if (valvec) + free(valvec); + + unchunk_group(__FUNCTION__); + return retval; +} + +/*! Modify database provided an xml tree, a restconf api_path and an operation + * + * @param[in] h CLICON handle + * @param[in] db running or candidate + * @param[in] op OP_MERGE: just add it. + * OP_REPLACE: first delete whole database + * OP_NONE: operation attribute in xml determines operation + * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) + * @param[in] xt xml-tree. Top-level symbol is dummy + * @retval 0 OK + * @retval -1 Error + * example: + * container top { + * list list1 { + * key "key1 key2 key3"; + * is referenced as + * /restconf/data/top/list1=a,,foo + * @see xmldb_put + */ +static int +xmldb_put_restconf_api_path(clicon_handle h, + char *db, + enum operation_type op, + char *api_path, + cxobj *xt) +{ + int retval = -1; + yang_stmt *y = NULL; + yang_stmt *ykey; + char **vec = NULL; + int nvec; + int i; + char *name; + cg_var *cvi; + cvec *cvk = NULL; /* vector of index keys */ + char *val2; + cbuf *ckey=NULL; /* partial keys */ + cbuf *csubkey=NULL; /* partial keys */ + cbuf *crx=NULL; /* partial keys */ + char *keyname; + int exists; + int npairs; + struct db_pair *pairs; + yang_spec *yspec; + yang_stmt *ys; + char *filename = NULL; + char *key; + char *keys; + + yspec = clicon_dbspec_yang(h); + if (db2file(h, db, &filename) < 0) + goto done; + if (api_path == NULL || *api_path!='/'){ + clicon_err(OE_DB, 0, "Invalid api path: %s", api_path); + goto done; + } + if ((ckey = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((csubkey = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL) + goto done; + if (nvec < 2){ + clicon_err(OE_XML, 0, "Malformed key: %s", api_path); + goto done; + } + i = 1; + while (iys_keyword == Y_LEAF && + y->ys_parent->yn_keyword == Y_LIST && + yang_key_match(y->ys_parent, y->ys_argument)) + /* Special rule if key, dont write last key-name, rm whole*/; + else + cprintf(ckey, "/%s", name); + i++; + switch (y->ys_keyword){ + case Y_LEAF_LIST: + /* For leaf-list 'keys' is value, see 3.5.1 in restconf draft */ + val2 = keys; + cprintf(ckey, "/%s", keys); + break; + case Y_LIST: + if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, y->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + cvi = NULL; + /* Iterate over individual yang keys */ + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + // val2 = vec[i++]; /* No */ + val2 = keys; + if (i>nvec){ /* XXX >= ? */ + clicon_err(OE_XML, errno, "List %s without argument", name); + goto done; + } + cprintf(ckey, "=%s", val2); + cbuf_reset(csubkey); + cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); + if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) + if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0) + goto done; + } + if (cvk){ + cvec_free(cvk); + cvk = NULL; + } + break; + default: + if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) + if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0) + goto done; + break; + } + } + key = cbuf_get(ckey); + /* final key */ + switch (op){ + case OP_CREATE: + if ((exists = db_exists(filename, key)) < 0) + goto done; + if (exists == 1){ + clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", key); + goto done; + } + case OP_MERGE: + case OP_REPLACE: + if (xt==NULL){ + clicon_err(OE_DB, 0, "%s: no xml when yang node %s required", + __FUNCTION__, y->ys_argument); + goto done; + } + if ((ys = yang_find_syntax((yang_node*)y, xml_name(xt))) == NULL){ + clicon_err(OE_DB, 0, "%s: child %s not found under node %s", + __FUNCTION__, xml_name(xt), y->ys_argument); + goto done; + } + y = ys; + if (put(filename, xt, y, op, key) < 0) + goto done; + break; + case OP_DELETE: + if ((exists = db_exists(filename, key)) < 0) + goto done; + if (exists == 0){ + clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", key); + goto done; + } + case OP_REMOVE: + /* Read in complete database (this can be optimized) */ + if ((crx = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(crx, "^%s.*$", key); + if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0) + goto done; + for (i = 0; i < npairs; i++) { + if (db_del(filename, pairs[i].dp_key) < 0) + goto done; + } + break; + default: + break; + } + retval = 0; + done: + // clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (filename) + free(filename); + if (ckey) + cbuf_free(ckey); + if (csubkey) + cbuf_free(csubkey); + if (crx) + cbuf_free(crx); + if (cvk) + cvec_free(cvk); + if (vec) + free(vec); + unchunk_group(__FUNCTION__); + return retval; +} + +/*! Modify database provided an xml tree and an operation + * + * @param[in] h CLICON handle + * @param[in] db running or candidate + * @param[in] xt xml-tree. Top-level symbol is dummy + * @param[in] op OP_MERGE: just add it. + * OP_REPLACE: first delete whole database + * OP_NONE: operation attribute in xml determines operation + * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) + * @retval 0 OK + * @retval -1 Error + * The xml may contain the "operation" attribute which defines the operation. + * @code + * cxobj *xt; + * if (clicon_xml_parse_str("17", &xt) < 0) + * err; + * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) + * err; + * @endcode + * @see xmldb_put_xkey for single key + */ +int +kv_put(clicon_handle h, + char *db, + enum operation_type op, + char *api_path, + cxobj *xt) +{ + int retval = -1; + cxobj *x = NULL; + yang_stmt *ys; + yang_spec *yspec; + char *dbfilename = NULL; + + if (xml_child_nr(xt)==0 || xml_body(xt)!= NULL) + return xmldb_put_xkey(h, db, op, api_path, xml_body(xt)); + yspec = clicon_dbspec_yang(h); + if (db2file(h, db, &dbfilename) < 0) + goto done; + if (op == OP_REPLACE){ + if (db_delete(dbfilename) < 0) + goto done; + if (db_init(dbfilename) < 0) + goto done; + } + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ + if (api_path && strlen(api_path)){ + if (xmldb_put_restconf_api_path(h, db, op, api_path, x) < 0) + goto done; + continue; + } + if ((ys = yang_find_topnode(yspec, xml_name(x))) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x)); + goto done; + } + if (put(dbfilename, /* database name */ + x, /* xml root node */ + ys, /* yang statement of xml node */ + op, /* operation, eg merge/delete */ + "" /* aggregate xml key */ + ) < 0) + goto done; + } + retval = 0; + done: + if (dbfilename) + free(dbfilename); + return retval; +} + + +/*! Raw dump of database, just keys and values, no xml interpretation + * @param[in] f File + * @param[in] dbfile File-name of database. This is a local file + * @param[in] rxkey Key regexp, eg "^.*$" + * @note This function can only be called locally. + */ +int +kv_dump(FILE *f, + char *dbfilename, + char *rxkey) +{ + int retval = -1; + int npairs; + struct db_pair *pairs; + + /* Default is match all */ + if (rxkey == NULL) + rxkey = "^.*$"; + + /* Get all keys/values for vector */ + if ((npairs = db_regexp(dbfilename, rxkey, __FUNCTION__, &pairs, 0)) < 0) + goto done; + + for (npairs--; npairs >= 0; npairs--) + fprintf(f, "%s %s\n", pairs[npairs].dp_key, + pairs[npairs].dp_val?pairs[npairs].dp_val:""); + retval = 0; + done: + unchunk_group(__FUNCTION__); + return retval; +} + +/*! Copy database from db1 to db2 + * @param[in] h Clicon handle + * @param[in] from Source database copy + * @param[in] to Destination database + * @retval -1 Error + * @retval 0 OK + */ +int +kv_copy(clicon_handle h, + char *from, + char *to) +{ + int retval = -1; + char *fromfile = NULL; + char *tofile = NULL; + + /* XXX lock */ + if (db2file(h, from, &fromfile) < 0) + goto done; + if (db2file(h, to, &tofile) < 0) + goto done; + if (clicon_file_copy(fromfile, tofile) < 0) + goto done; + retval = 0; + done: + if (fromfile) + free(fromfile); + if (tofile) + free(tofile); + return retval; +} + +/*! Lock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + */ +int +kv_lock(clicon_handle h, + char *db, + int pid) +{ + int retval = -1; + + if (db_islocked(db)){ + if (pid != db_islocked(db)){ + clicon_err(OE_DB, 0, "lock failed: locked by %d", db_islocked(db)); + goto done; + } + } + else + db_lock(db, pid); + retval = 0; + done: + return retval; +} + +/*! Unlock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + */ +int +kv_unlock(clicon_handle h, + char *db, + int pid) +{ + int retval = -1; + int pid1; + + pid1 = db_islocked(db); + if (pid1){ + if (pid == pid1) + db_unlock(db); + else{ + clicon_err(OE_DB, 0, "unlock failed: locked by %d", pid1); + goto done; + } + } + retval = 0; + done: + return retval; +} + +/*! Unlock all databases locked by pid (eg process dies) + */ +int +kv_unlock_all(clicon_handle h, + int pid) +{ + return db_unlock_all(pid); +} + +/*! Check if database is locked + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 Not locked + * @retval >0 Id of locker + */ +int +kv_islocked(clicon_handle h, + char *db) +{ + return db_islocked(db); +} + +/*! Check if db exists + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 No it does not exist + * @retval 1 Yes it exists + */ +int +kv_exists(clicon_handle h, + char *db) +{ + int retval = -1; + char *filename = NULL; + struct stat sb; + + if (db2file(h, db, &filename) < 0) + goto done; + if (lstat(filename, &sb) < 0) + retval = 0; + else + retval = 1; + done: + if (filename) + free(filename); + return retval; +} + +/*! Delete database. Remove file + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 OK + */ +int +kv_delete(clicon_handle h, + char *db) +{ + int retval = -1; + char *filename = NULL; + + if (db2file(h, db, &filename) < 0) + goto done; + if (db_delete(filename) < 0) + goto done; + retval = 0; + done: + if (filename) + free(filename); + return retval; +} + +/*! Initialize database + * @param[in] h Clicon handle + * @param[in] db Database + * @retval 0 OK + * @retval -1 Error + */ +int +kv_init(clicon_handle h, + char *db) +{ + int retval = -1; + char *filename = NULL; + + if (db2file(h, db, &filename) < 0) + goto done; + if (db_init(filename) < 0) + goto done; + retval = 0; + done: + if (filename) + free(filename); + return retval; +} + +/*! plugin init function */ +int +keyvalue_plugin_exit(void) +{ + return 0; +} + +static const struct xmldb_api api; + +/*! plugin init function */ +void * +clixon_xmldb_plugin_init(int version) +{ + if (version != XMLDB_API_VERSION){ + clicon_err(OE_DB, 0, "Invalid version %d expected %d", + version, XMLDB_API_VERSION); + goto done; + } + return (void*)&api; + done: + return NULL; +} + +static const struct xmldb_api api = { + 1, + XMLDB_API_MAGIC, + clixon_xmldb_plugin_init, + keyvalue_plugin_exit, + kv_get, + kv_put, + kv_dump, + kv_copy, + kv_lock, + kv_unlock, + kv_unlock_all, + kv_islocked, + kv_exists, + kv_delete, + kv_init, +}; + + +#if 0 /* Test program */ +/* + * Turn this on to get an xpath test program + * Usage: clicon_xpath [] + * read xml from input + * Example compile: + gcc -g -o xmldb -I. -I../clixon ./clixon_xmldb.c -lclixon -lcligen +*/ + +static int +usage(char *argv0) +{ + fprintf(stderr, "usage:\n%s\tget []\t\txml on stdin\n", argv0); + fprintf(stderr, "\tput set|merge|delete\txml to stdout\n"); + exit(0); +} + +int +main(int argc, char **argv) +{ + cxobj *xt; + cxobj *xn; + char *xpath; + enum operation_type op; + char *cmd; + char *db; + char *yangdir; + char *yangmod; + yang_spec *yspec = NULL; + clicon_handle h; + + if ((h = clicon_handle_init()) == NULL) + goto done; + clicon_log_init("xmldb", LOG_DEBUG, CLICON_LOG_STDERR); + if (argc < 4){ + usage(argv[0]); + goto done; + } + cmd = argv[1]; + db = argv[2]; + yangdir = argv[3]; + yangmod = argv[4]; + db_init(db); + if ((yspec = yspec_new()) == NULL) + goto done + if (yang_parse(h, yangdir, yangmod, NULL, yspec) < 0) + goto done; + if (strcmp(cmd, "get")==0){ + if (argc < 5) + usage(argv[0]); + xpath = argc>5?argv[5]:NULL; + if (xmldb_get(h, db, xpath, &xt, NULL, NULL) < 0) + goto done; + clicon_xml2file(stdout, xt, 0, 1); + } + else + if (strcmp(cmd, "put")==0){ + if (argc != 6) + usage(argv[0]); + if (clicon_xml_parse_file(0, &xt, "") < 0) + goto done; + if (xml_rootchild(xt, 0, &xn) < 0) + goto done; + if (strcmp(argv[5], "set") == 0) + op = OP_REPLACE; + else + if (strcmp(argv[4], "merge") == 0) + op = OP_MERGE; + else if (strcmp(argv[5], "delete") == 0) + op = OP_REMOVE; + else + usage(argv[0]); + if (xmldb_put(h, db, op, NULL, xn) < 0) + goto done; + } + else + usage(argv[0]); + printf("\n"); + done: + return 0; +} + +#endif /* Test program */ diff --git a/datastore/keyvalue/clixon_keyvalue.h b/datastore/keyvalue/clixon_keyvalue.h new file mode 100644 index 00000000..ed5cea51 --- /dev/null +++ b/datastore/keyvalue/clixon_keyvalue.h @@ -0,0 +1,56 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + Key-value store + */ +#ifndef _CLIXON_KEYVALUE_H +#define _CLIXON_KEYVALUE_H + +/* + * Prototypes + */ +int kv_get(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen); +int kv_put(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt); +int kv_dump(FILE *f, char *dbfilename, char *rxkey); +int kv_copy(clicon_handle h, char *from, char *to); +int kv_lock(clicon_handle h, char *db, int pid); +int kv_unlock(clicon_handle h, char *db, int pid); +int kv_unlock_all(clicon_handle h, int pid); +int kv_islocked(clicon_handle h, char *db); +int kv_exists(clicon_handle h, char *db); +int kv_delete(clicon_handle h, char *db); +int kv_init(clicon_handle h, char *db); + +#endif /* _CLIXON_KEYVALUE_H */ diff --git a/lib/src/clixon_qdb.c b/datastore/keyvalue/clixon_qdb.c similarity index 100% rename from lib/src/clixon_qdb.c rename to datastore/keyvalue/clixon_qdb.c diff --git a/lib/src/clixon_qdb.h b/datastore/keyvalue/clixon_qdb.h similarity index 100% rename from lib/src/clixon_qdb.h rename to datastore/keyvalue/clixon_qdb.h diff --git a/lib/clixon/clixon_options.h b/lib/clixon/clixon_options.h index a86f618f..859bfb49 100644 --- a/lib/clixon/clixon_options.h +++ b/lib/clixon/clixon_options.h @@ -93,6 +93,7 @@ char *clicon_clispec_dir(clicon_handle h); char *clicon_netconf_dir(clicon_handle h); char *clicon_restconf_dir(clicon_handle h); char *clicon_archive_dir(clicon_handle h); +char *clicon_xmldb_plugin(clicon_handle h); int clicon_sock_family(clicon_handle h); char *clicon_sock(clicon_handle h); int clicon_sock_port(clicon_handle h); diff --git a/lib/clixon/clixon_xml_db.h b/lib/clixon/clixon_xml_db.h index 8889a8d8..04325360 100644 --- a/lib/clixon/clixon_xml_db.h +++ b/lib/clixon/clixon_xml_db.h @@ -30,18 +30,83 @@ the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** - - * XML support functions. */ #ifndef _CLIXON_XML_DB_H #define _CLIXON_XML_DB_H +/* Version of clixon datastore plugin API. */ +#define XMLDB_API_VERSION 1 + +/* Magic to ensure plugin sanity. */ +#define XMLDB_API_MAGIC 0xf386f730 + +/* Name of plugin init function (must be called this) */ +#define XMLDB_PLUGIN_INIT_FN "clixon_xmldb_plugin_init" + +/* Type of plugin init function */ +typedef void * (plugin_init_t)(int version); + +/* Type of plugin exit function */ +typedef int (plugin_exit_t)(void); + +/* Type of xmldb get function */ +typedef int (xmldb_get_t)(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen); + +/* Type of xmldb put function */ +typedef int (xmldb_put_t)(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt); + +/* Type of xmldb dump function */ +typedef int (xmldb_dump_t)(FILE *f, char *dbfilename, char *rxkey); + +/* Type of xmldb copy function */ +typedef int (xmldb_copy_t)(clicon_handle h, char *from, char *to); + +/* Type of xmldb lock function */ +typedef int (xmldb_lock_t)(clicon_handle h, char *db, int pid); + +/* Type of xmldb unlock function */ +typedef int (xmldb_unlock_t)(clicon_handle h, char *db, int pid); + +/* Type of xmldb unlock_all function */ +typedef int (xmldb_unlock_all_t)(clicon_handle h, int pid); + +/* Type of xmldb islocked function */ +typedef int (xmldb_islocked_t)(clicon_handle h, char *db); + +/* Type of xmldb exists function */ +typedef int (xmldb_exists_t)(clicon_handle h, char *db); + +/* Type of xmldb delete function */ +typedef int (xmldb_delete_t)(clicon_handle h, char *db); + +/* Type of xmldb init function */ +typedef int (xmldb_init_t)(clicon_handle h, char *db); + +/* grideye agent plugin init struct for the api */ +struct xmldb_api{ + int xa_version; + int xa_magic; + plugin_init_t *xa_plugin_init_fn; /* XMLDB_PLUGIN_INIT_FN */ + plugin_exit_t *xa_plugin_exit_fn; + xmldb_get_t *xa_get_fn; + xmldb_put_t *xa_put_fn; + xmldb_dump_t *xa_dump_fn; + xmldb_copy_t *xa_copy_fn; + xmldb_lock_t *xa_lock_fn; + xmldb_unlock_t *xa_unlock_fn; + xmldb_unlock_all_t *xa_unlock_all_fn; + xmldb_islocked_t *xa_islocked_fn; + xmldb_exists_t *xa_exists_fn; + xmldb_delete_t *xa_delete_fn; + xmldb_init_t *xa_init_fn; +}; + /* * Prototypes */ -int yang2xmlkeyfmt(yang_stmt *ys, int inclkey, char **xkfmt); -int xmlkeyfmt2key(char *xkfmt, cvec *cvv, char **xk); -int xmlkeyfmt2xpath(char *xkfmt, cvec *cvv, char **xk); +int xmldb_plugin_load(char *filename); int xmldb_get(clicon_handle h, char *db, char *xpath, cxobj **xtop, cxobj ***xvec, size_t *xlen); diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index f90a8e71..7cbc4680 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -61,5 +61,8 @@ int xml_diff(yang_spec *yspec, cxobj *xt1, cxobj *xt2, cxobj ***first, size_t *firstlen, cxobj ***second, size_t *secondlen, cxobj ***changed1, cxobj ***changed2, size_t *changedlen); +int yang2xmlkeyfmt(yang_stmt *ys, int inclkey, char **xkfmt); +int xmlkeyfmt2key(char *xkfmt, cvec *cvv, char **xk); +int xmlkeyfmt2xpath(char *xkfmt, cvec *cvv, char **xk); #endif /* _CLIXON_XML_MAP_H_ */ diff --git a/lib/src/Makefile.in b/lib/src/Makefile.in index ac869185..35221e22 100644 --- a/lib/src/Makefile.in +++ b/lib/src/Makefile.in @@ -61,15 +61,14 @@ CPPFLAGS = @CPPFLAGS@ INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$(top_srcdir) -SRC = clixon_sig.c clixon_qdb.c clixon_log.c clixon_err.c clixon_event.c \ +SRC = clixon_sig.c clixon_log.c clixon_err.c clixon_event.c \ clixon_chunk.c clixon_proc.c \ clixon_string.c clixon_handle.c \ clixon_xml.c clixon_xml_map.c clixon_file.c \ - clixon_json.c \ - clixon_yang.c clixon_yang_type.c \ + clixon_json.c clixon_yang.c clixon_yang_type.c \ clixon_hash.c clixon_options.c clixon_plugin.c \ clixon_proto.c clixon_proto_client.c \ - clixon_xsl.c clixon_sha1.c clixon_xml_db.c + clixon_xsl.c clixon_sha1.c clixon_xml_db.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_options.c b/lib/src/clixon_options.c index 32df541e..11b6d403 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -237,6 +237,10 @@ clicon_option_sanity(clicon_hash_t *copt) clicon_err(OE_UNIX, 0, "CLICON_ARCHIVE_DIR not defined in config file"); goto done; } + if (!hash_lookup(copt, "CLICON_XMLDB_DIR")){ + clicon_err(OE_UNIX, 0, "CLICON_XMLDB_DIR not defined in config file"); + goto done; + } if (!hash_lookup(copt, "CLICON_SOCK")){ clicon_err(OE_UNIX, 0, "CLICON_SOCK not defined in config file"); goto done; @@ -453,6 +457,12 @@ clicon_archive_dir(clicon_handle h) return clicon_option_str(h, "CLICON_ARCHIVE_DIR"); } +char * +clicon_xmldb_plugin(clicon_handle h) +{ + return clicon_option_str(h, "CLICON_XMLDB_PLUGIN"); +} + /* get family of backend socket: AF_UNIX, AF_INET or AF_INET6 */ int clicon_sock_family(clicon_handle h) diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index d79a93ff..66350e48 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -31,1974 +31,278 @@ ***** END LICENSE BLOCK ***** */ -/* - * An xml database consists of key-value pairs for xml-trees. - * Each node in an xml-tree has a key and an optional value. - * The key (xmlkey) is constructed from the xml node name concatenated - * with its ancestors and any eventual list keys. - * A xmlkeyfmt is a help-structure used when accessing the XML database. - * It consists of an xmlkey but with the key fields replaced with wild-chars(%s) - * Example: /aaa/bbb/%s/%s/ccc - * Such an xmlkeyfmt can be obtained from a yang-statement by following - * its ancestors to the root module. If one of the ancestors is a list, - * a wildchar (%s) is inserted for each key. - * These xmlkeyfmt keys are saved and used in cli callbacks such as when - * modifying syntax (eg cli_merge/cli_delete) or when completing for sub-symbols - * In this case, the variables are set and the wildcards can be instantiated. - * An xml tree can then be formed that can be used to the xmldb_get() or - * xmldb_put() functions. - * The relations between the functions and formats are as follows: - * - * +-----------------+ +-----------------+ - * | yang-stmt | yang2xmlkeyfmt | xmlkeyfmt | xmlkeyfmt2xpath - * | list aa,leaf k | ----------------->| /aa=%s |----------------> - * +-----------------+ +-----------------+ - * | - * | xmlkeyfmt2key - * | k=17 - * v - * +-------------------+ +-----------------+ - * | xml-tree/cxobj | xmlkey2xml | xmlkey RFC3986| - * | 17| <------------- | /aa=17 | - * +-------------------+ +-----------------+ - * - * Alternative for xmlkeyfmt would be eg: - * RESTCONF: /interfaces/interface=%s/ipv4/address/ip=%s (used) - * XPATH: /interfaces/interface[name=%s]/ipv4/address/[ip=%s] - * - * Paths through the code (for coverage) - * cli_callback_generate +----------------+ - * cli_expand_var_generate | yang2xmlkeyfmt | - * yang -------------> | | - * +----------------+ - * xmldb_get_tree - * - compare_dbs - * - netconf - * - validate - * - from_client_save - * - * xmldb_get_vec - * - restconf - * - expand_dbvar - * - show_conf_xpath - */ -#ifdef HAVE_CONFIG_H -#include "clixon_config.h" /* generated by config & autoconf */ -#endif -#include #include +#include #include #include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include +#include +#include +#include +#include +#include +#include +#include #include +#include /* cligen */ #include -/* clicon */ +/* clixon */ #include "clixon_err.h" #include "clixon_log.h" -#include "clixon_file.h" #include "clixon_queue.h" -#include "clixon_string.h" -#include "clixon_chunk.h" #include "clixon_hash.h" #include "clixon_handle.h" -#include "clixon_qdb.h" -#include "clixon_yang.h" -#include "clixon_handle.h" #include "clixon_xml.h" -#include "clixon_options.h" -#include "clixon_xsl.h" -#include "clixon_xml_parse.h" #include "clixon_xml_db.h" -/*! Construct an xml key format from yang statement using wildcards for keys - * Recursively construct it to the top. - * Example: - * yang: container a -> list b -> key c -> leaf d - * xpath: /a/b/%s/d - * @param[in] ys Yang statement - * @param[in] inclkey If inclkey then include key leaf (eg last leaf d in ex) - * @param[out] cbuf keyfmt - */ -static int -yang2xmlkeyfmt_1(yang_stmt *ys, - int inclkey, - cbuf *cb) -{ - yang_node *yp; /* parent */ - yang_stmt *ykey; - int i; - cvec *cvk = NULL; /* vector of index keys */ - int retval = -1; +static struct xmldb_api *_xa_api = NULL; - yp = ys->ys_parent; - if (yp != NULL && - yp->yn_keyword != Y_MODULE && - yp->yn_keyword != Y_SUBMODULE){ - if (yang2xmlkeyfmt_1((yang_stmt *)yp, 1, cb) < 0) - goto done; - } - if (inclkey){ - if (ys->ys_keyword != Y_CHOICE && ys->ys_keyword != Y_CASE) - cprintf(cb, "/%s", ys->ys_argument); - } - else{ - if (ys->ys_keyword == Y_LEAF && yp && yp->yn_keyword == Y_LIST){ - if (yang_key_match(yp, ys->ys_argument) == 0) - cprintf(cb, "/%s", ys->ys_argument); /* Not if leaf and key */ - } - else - if (ys->ys_keyword != Y_CHOICE && ys->ys_keyword != Y_CASE) - cprintf(cb, "/%s", ys->ys_argument); - } - - switch (ys->ys_keyword){ - case Y_LIST: - if ((ykey = yang_find((yang_node*)ys, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, ys->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - if (cvec_len(cvk)) - cprintf(cb, "="); - /* Iterate over individual keys */ - for (i=0; i list b -> key c -> leaf d - * xpath: /a/b=%s/d - * @param[in] ys Yang statement - * @param[in] inclkey If !inclkey then dont include key leaf - * @param[out] xkfmt XML key format. Needs to be freed after use. - */ -int -yang2xmlkeyfmt(yang_stmt *ys, - int inclkey, - char **xkfmt) -{ - int retval = -1; - cbuf *cb = NULL; - - if ((cb = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - if (yang2xmlkeyfmt_1(ys, inclkey, cb) < 0) - goto done; - if ((*xkfmt = strdup(cbuf_get(cb))) == NULL){ - clicon_err(OE_UNIX, errno, "strdup"); - goto done; - } - retval = 0; - done: - if (cb) - cbuf_free(cb); - return retval; -} - - -/*! Transform an xml key format and a vector of values to an XML key - * Used for actual key, eg in clicon_rpc_change(), xmldb_put_xkey() - * Example: - * xmlkeyfmt: /aaa/%s - * cvv: key=17 - * xmlkey: /aaa/17 - * @param[in] xkfmt XML key format, eg /aaa/%s - * @param[in] cvv cligen variable vector, one for every wildchar in xkfmt - * @param[out] xk XML key, eg /aaa/17. Free after use - * @note first and last elements of cvv are not used,.. - * @see cli_dbxml where this function is called - */ -int -xmlkeyfmt2key(char *xkfmt, - cvec *cvv, - char **xk) -{ - int retval = -1; - char c; - int esc=0; - cbuf *cb = NULL; - int i; - int j; - char *str; - char *strenc=NULL; - - - /* Sanity check */ -#if 1 - j = 0; /* Count % */ - for (i=0; i0 Id of locker - */ -static int -db_islocked(char *db) -{ - if (strcmp("running", db) == 0) - return (_running_locked); - else if (strcmp("candidate", db) == 0) - return(_candidate_locked); - else if (strcmp("startup", db) == 0) - return(_startup_locked); - return 0; -} - - - -/*! Translate from symbolic database name to actual filename in file-system - * @param[in] h Clicon handle - * @param[in] db Symbolic database name, eg "candidate", "running" - * @param[out] filename Filename. Unallocate after use with free() - * @retval 0 OK - * @retval -1 Error - * @note Could need a way to extend which databases exists, eg to register new. - * The currently allowed databases are: - * candidate, tmp, running, result - * The filename reside in CLICON_XMLDB_DIR option - */ -static int -db2file(clicon_handle h, - char *db, - char **filename) -{ - int retval = -1; - cbuf *cb; - char *dir; - - if ((cb = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - goto done; - } - if ((dir = clicon_xmldb_dir(h)) == NULL){ - clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set"); - goto done; - } - if (strcmp(db, "running") != 0 && - strcmp(db, "candidate") != 0 && - strcmp(db, "startup") != 0 && - strcmp(db, "tmp") != 0){ - clicon_err(OE_XML, 0, "Unexpected database: %s", db); - goto done; - } - cprintf(cb, "%s/%s_db", dir, db); - if ((*filename = strdup4(cbuf_get(cb))) == NULL){ - clicon_err(OE_UNIX, errno, "strdup"); - goto done; - } - retval = 0; - done: - if (cb) - cbuf_free(cb); - return retval; -} - -/*! Help function to append key values from an xml list to a cbuf - * Example, a yang node x with keys a and b results in "x/a/b" - */ -static int -append_listkeys(cbuf *ckey, - cxobj *xt, - yang_stmt *ys) -{ - int retval = -1; - yang_stmt *ykey; - cxobj *xkey; - cg_var *cvi; - cvec *cvk = NULL; /* vector of index keys */ - char *keyname; - char *bodyenc; - int i=0; - - if ((ykey = yang_find((yang_node*)ys, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, ys->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - cvi = NULL; - /* Iterate over individual keys */ - while ((cvi = cvec_each(cvk, cvi)) != NULL) { - keyname = cv_string_get(cvi); - if ((xkey = xml_find(xt, keyname)) == NULL){ - clicon_err(OE_XML, errno, "XML list node \"%s\" does not have key \"%s\" child", - xml_name(xt), keyname); - goto done; - } - if ((bodyenc = curl_easy_escape(NULL, xml_body(xkey), 0)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); - goto done; - } - if (i++) - cprintf(ckey, ","); - else - cprintf(ckey, "="); - cprintf(ckey, "%s", bodyenc); - free(bodyenc); bodyenc = NULL; - } - retval = 0; - done: - if (cvk) - cvec_free(cvk); - return retval; -} - -/*! Help function to create xml key values - * @param[in,out] x Parent - * @param[in] ykey - * @param[in] arg - * @param[in] keyname yang key name - */ -static int -create_keyvalues(cxobj *x, - yang_stmt *ykey, - char *arg, - char *keyname) -{ - int retval = -1; - cxobj *xb; - - /* Check if key node exists */ - if ((xb = xml_new_spec(keyname, x, ykey)) == NULL) - goto done; - if ((xb = xml_new("body", xb)) == NULL) - goto done; - xml_type_set(xb, CX_BODY); - xml_value_set(xb, arg); - retval = 0; - done: - return retval; -} - -/*! Prune everything that has not been marked - * @param[in] xt XML tree with some node marked - * @param[out] upmark Set if a child (recursively) has marked set. - * The function removes all branches that does not contain a marked child - * XXX: maybe key leafs should not be purged if list is not purged? - * XXX: consider move to clicon_xml - */ -static int -xml_tree_prune_unmarked(cxobj *xt, - int *upmark) -{ - int retval = -1; - int submark; - int mark; - cxobj *x; - cxobj *xprev; - - mark = 0; - x = NULL; - xprev = x = NULL; - while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { - if (xml_flag(x, XML_FLAG_MARK)){ - mark++; - xprev = x; - continue; /* mark and stop here */ - } - if (xml_tree_prune_unmarked(x, &submark) < 0) - goto done; - if (submark) - mark++; - else{ /* Safe with xml_child_each if last */ - if (xml_purge(x) < 0) - goto done; - x = xprev; - } - xprev = x; - } - retval = 0; - done: - if (upmark) - *upmark = mark; - return retval; -} - -/*! - * @param[in] xk xmlkey - * @param[out] xt XML tree as result - * XXX cannot handle top-level list - */ -static int -get(char *dbname, - yang_spec *ys, - char *xk, - char *val, - cxobj *xt) -{ - int retval = -1; - char **vec = NULL; - int nvec; - char **valvec = NULL; - int nvalvec; - int i; - int j; - char *name; - char *restval; - yang_stmt *y; - cxobj *x; - cxobj *xc; - cxobj *xb; - yang_stmt *ykey; - cg_var *cvi; - cvec *cvk = NULL; /* vector of index keys */ - char *keyname; - char *arg; - char *argdec; - cbuf *cb; - - // clicon_debug(1, "%s xkey:%s val:%s", __FUNCTION__, xk, val); - x = xt; - if (xk == NULL || *xk!='/'){ - clicon_err(OE_DB, 0, "Invalid key: %s", xk); - goto done; - } - if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) - goto done; - /* Element 0 is NULL '/', - Element 1 is top symbol and needs to find subs in all modules: - spec->module->syntaxnode - */ - if (nvec < 2){ - clicon_err(OE_XML, 0, "Malformed key: %s", xk); - goto done; - } - i = 1; - while (i name:x restval=1,2 */ - if ((restval = index(name, '=')) != NULL){ - *restval = '\0'; - restval++; - } - if (i == 1){ /* spec->module->node */ - if ((y = yang_find_topnode(ys, name)) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", name); - goto done; - } - } - else - if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", name); - goto done; - } - switch (y->ys_keyword){ - case Y_LEAF_LIST: - /* - * If xml element is a leaf-list, then the next element is expected to - * be a value - */ - if ((argdec = curl_easy_unescape(NULL, restval, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); - goto done; - } - if ((xc = xml_find(x, name))==NULL || - (xb = xml_find(xc, argdec))==NULL){ - if ((xc = xml_new_spec(name, x, y)) == NULL) - goto done; - /* Assume body is created at end of function */ - } - free(argdec); - argdec = NULL; - break; - case Y_LIST: - /* - * If xml element is a list, then the next element(s) is expected to be - * a key value. Check if this key value is already in the xml tree, - * otherwise create it. - */ - if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, y->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - if ((cb = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - goto done; - } - cvi = NULL; - /* Iterate over individual yang keys */ - cprintf(cb, "%s", name); - if (valvec) - free(valvec); - if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) - goto done; - if (cvec_len(cvk)!=nvalvec){ - retval = 0; - goto done; - } - j = 0; - while ((cvi = cvec_each(cvk, cvi)) != NULL){ - if (j>=nvalvec) - break; - arg = valvec[j++]; - if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); - goto done; - } - cprintf(cb, "[%s=%s]", cv_string_get(cvi), argdec); - free(argdec); - argdec=NULL; - } - if ((xc = xpath_first(x, cbuf_get(cb))) == NULL){ - if ((xc = xml_new_spec(name, x, y)) == NULL) - goto done; - cvi = NULL; - // i -= cvec_len(cvk); - /* Iterate over individual yang keys */ - j=0; - while ((cvi = cvec_each(cvk, cvi)) != NULL) { - if (j>=nvalvec) - break; - arg = valvec[j++]; - keyname = cv_string_get(cvi); - if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); - goto done; - } - if (create_keyvalues(xc, - ykey, - argdec, - keyname) < 0) - goto done; - free(argdec); argdec = NULL; - } /* while */ - } - if (cb){ - cbuf_free(cb); - cb = NULL; - } - if (cvk){ - cvec_free(cvk); - cvk = NULL; - } - break; - case Y_LEAF: - case Y_CONTAINER: - default: - if ((xc = xml_find(x, name))==NULL) - if ((xc = xml_new_spec(name, x, y)) == NULL) - goto done; - break; - } /* switch */ - x = xc; - i++; - } - if (val && xml_body(x)==NULL){ - if ((x = xml_new("body", x)) == NULL) - goto done; - xml_type_set(x, CX_BODY); - xml_value_set(x, val); - } - if(debug>1){ - fprintf(stderr, "%s %s\n", __FUNCTION__, xk); - clicon_xml2file(stderr, xt, 0, 1); - } - retval = 0; - done: - if (vec) - free(vec); - if (valvec) - free(valvec); - if (cvk) - cvec_free(cvk); - return retval; -} - -/*! Sanitize an xml tree: xml node has matching yang_stmt pointer - */ -static int -xml_sanity(cxobj *x, - void *arg) -{ - int retval = -1; - yang_stmt *ys; - - ys = (yang_stmt*)xml_spec(x); - if (ys==NULL){ - clicon_err(OE_XML, 0, "No spec for xml node %s", xml_name(x)); - goto done; - } - if (strstr(ys->ys_argument, xml_name(x))==NULL){ - clicon_err(OE_XML, 0, "xml node name '%s' does not match yang spec arg '%s'", - xml_name(x), ys->ys_argument); - goto done; - } - retval = 0; - done: - return retval; -} - -/*! Add default values (if not set) - */ -static int -xml_default(cxobj *x, - void *arg) -{ - int retval = -1; - yang_stmt *ys; - yang_stmt *y; - int i; - cxobj *xc; - cxobj *xb; - char *str; - - ys = (yang_stmt*)xml_spec(x); - /* Check leaf defaults */ - if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){ - for (i=0; iys_len; i++){ - y = ys->ys_stmt[i]; - if (y->ys_keyword != Y_LEAF) - continue; - assert(y->ys_cv); - if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */ - if (!xml_find(x, y->ys_argument)){ - if ((xc = xml_new_spec(y->ys_argument, x, y)) == NULL) - goto done; - if ((xb = xml_new("body", xc)) == NULL) - goto done; - xml_type_set(xb, CX_BODY); - if ((str = cv2str_dup(y->ys_cv)) == NULL){ - clicon_err(OE_UNIX, errno, "cv2str_dup"); - goto done; - } - if (xml_value_set(xb, str) < 0) - goto done; - free(str); - } - } - } - } - retval = 0; - done: - return retval; -} - -/*! Order XML children according to YANG - */ -static int -xml_order(cxobj *x, - void *arg) -{ - int retval = -1; - yang_stmt *y; - yang_stmt *yc; - int i; - int j0; - int j; - cxobj *xc; - cxobj *xj; - char *yname; /* yang child name */ - char *xname; /* xml child name */ - - y = (yang_stmt*)xml_spec(x); - j0 = 0; - /* Go through xml children and ensure they are same order as yspec children */ - for (i=0; iys_len; i++){ - yc = y->ys_stmt[i]; - if (!yang_is_syntax(yc)) - continue; - yname = yc->ys_argument; - /* First go thru xml children with same name */ - for (;j01) - clicon_xml2file(stderr, xt, 0, 1); - *xtop = xt; - retval = 0; - done: - if (dbname) - free(dbname); - if (xvec) - free(xvec); - unchunk_group(__FUNCTION__); - return retval; - -} - -/*! Add data to database internal recursive function - * @param[in] dbname Name of database to search in (filename incl dir path) - * @param[in] xt xml-node. - * @param[in] ys Yang statement corresponding to xml-node - * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc - * @param[in] xkey0 aggregated xmlkey - * @retval 0 OK - * @retval -1 Error - * @note XXX op only supports merge - */ -static int -put(char *dbname, - cxobj *xt, - yang_stmt *ys, - enum operation_type op, - const char *xk0) -{ - int retval = -1; - cxobj *x = NULL; - char *xk; - cbuf *cbxk = NULL; - char *body; - yang_stmt *y; - int exists; - char *bodyenc=NULL; - char *opstr; - - clicon_debug(1, "%s xk0:%s ys:%s", __FUNCTION__, xk0, ys->ys_argument); - if (debug){ - xml_print(stderr, xt); - // yang_print(stderr, (yang_node*)ys, 0); - } - if ((opstr = xml_find_value(xt, "operation")) != NULL) - if (xml_operation(opstr, &op) < 0) - goto done; - body = xml_body(xt); - if ((cbxk = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - cprintf(cbxk, "%s/%s", xk0, xml_name(xt)); - switch (ys->ys_keyword){ - case Y_LIST: /* Note: can have many keys */ - if (append_listkeys(cbxk, xt, ys) < 0) - goto done; - break; - case Y_LEAF_LIST: - if ((bodyenc = curl_easy_escape(NULL, body, 0)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); - goto done; - } - cprintf(cbxk, "=%s", bodyenc); - break; - default: - break; - } - xk = cbuf_get(cbxk); - // fprintf(stderr, "%s %s\n", key, body?body:""); - /* Write to database, key and a vector of variables */ - switch (op){ - case OP_CREATE: - if ((exists = db_exists(dbname, xk)) < 0) - goto done; - if (exists == 1){ - clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk); - goto done; - } - case OP_MERGE: - case OP_REPLACE: - if (db_set(dbname, xk, body?body:NULL, body?strlen(body)+1:0) < 0) - goto done; - break; - case OP_DELETE: - if ((exists = db_exists(dbname, xk)) < 0) - goto done; - if (exists == 0){ - clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); - goto done; - } - case OP_REMOVE: - if (db_del(dbname, xk) < 0) - goto done; - break; - case OP_NONE: - break; - } - /* For every node, create a key with values */ - while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ - if ((y = yang_find_syntax((yang_node*)ys, xml_name(x))) == NULL){ - clicon_err(OE_UNIX, 0, "No yang node found: %s", xml_name(x)); - goto done; - } - if (put(dbname, x, y, op, xk) < 0) - goto done; - } - retval = 0; - done: - if (cbxk) - cbuf_free(cbxk); - if (bodyenc) - free(bodyenc); - return retval; -} - -/*! Modify database provided an XML database key and an operation - * @param[in] h CLICON handle - * @param[in] db Database name - * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc - * @param[in] xk XML Key, eg /aa/bb=17/name - * @param[in] val Key value, eg "17" - - * @retval 0 OK - * @retval -1 Error - * @code - * if (xmldb_put_xkey(h, db, OP_MERGE, "/aa/bb=17/name", "17") < 0) - * err; - * @endcode - * @see xmldb_put with xml-tree, no path - */ -static int -xmldb_put_xkey(clicon_handle h, - char *db, - enum operation_type op, - char *xk, - char *val) - -{ - int retval = -1; - cxobj *x = NULL; - yang_stmt *y = NULL; - yang_stmt *ykey; - char **vec = NULL; - int nvec; - char **valvec = NULL; - int nvalvec; - int i; - int j; - char *name; - char *restval; - cg_var *cvi; - cvec *cvk = NULL; /* vector of index keys */ - char *val2 = NULL; - cbuf *ckey=NULL; /* partial keys */ - cbuf *csubkey=NULL; /* partial keys */ - cbuf *crx=NULL; /* partial keys */ - char *keyname; - int exists; - int npairs; - struct db_pair *pairs; - yang_spec *yspec; - char *filename = NULL; - - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &filename) < 0) - goto done; - if (xk == NULL || *xk!='/'){ - clicon_err(OE_DB, 0, "Invalid key: %s", xk); - goto done; - } - if ((ckey = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - if ((csubkey = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) - goto done; - if (nvec < 2){ - clicon_err(OE_XML, 0, "Malformed key: %s", xk); - goto done; - } - i = 1; - while (i name:x restval=1,2 */ - if ((restval = index(name, '=')) != NULL){ - *restval = '\0'; - restval++; - } - if (i==1){ - if (strlen(name)==0 && (op==OP_DELETE || op == OP_REMOVE)){ - /* Special handling of "/" */ - cprintf(ckey, "/"); - break; - } - else - if ((y = yang_find_topnode(yspec, name)) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", x?xml_name(x):""); - goto done; - } - } - else - if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", name); - goto done; - } - if ((op==OP_DELETE || op == OP_REMOVE) && - y->ys_keyword == Y_LEAF && - y->ys_parent->yn_keyword == Y_LIST && - yang_key_match(y->ys_parent, y->ys_argument)) - /* Special rule if key, dont write last key-name, rm whole*/; - else - cprintf(ckey, "/%s", name); - i++; - switch (y->ys_keyword){ - case Y_LEAF_LIST: - if (restval==NULL){ - clicon_err(OE_XML, 0, "malformed key, expected '='"); - goto done; - } - cprintf(ckey, "=%s", restval); - break; - case Y_LIST: - if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, y->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - if (restval==NULL){ - clicon_err(OE_XML, 0, "malformed key, expected '='"); - goto done; - } - if (valvec) - free(valvec); - if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) - goto done; - - if (cvec_len(cvk) != nvalvec){ - clicon_err(OE_XML, errno, "List %s key length mismatch", name); - goto done; - } - cvi = NULL; - /* Iterate over individual yang keys */ - j = 0; - while ((cvi = cvec_each(cvk, cvi)) != NULL) { - keyname = cv_string_get(cvi); - if (j) - cprintf(ckey, ","); - else - cprintf(ckey, "="); - val2 = valvec[j++]; - - cprintf(ckey, "%s", val2); - cbuf_reset(csubkey); - cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); - if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) - if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0) - goto done; - } - if (cvk){ - cvec_free(cvk); - cvk = NULL; - } - break; - default: - if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) - if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0) - goto done; - break; - } - } - xk = cbuf_get(ckey); - /* final key */ - switch (op){ - case OP_CREATE: - if ((exists = db_exists(filename, xk)) < 0) - goto done; - if (exists == 1){ - clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk); - goto done; - } - case OP_MERGE: - case OP_REPLACE: - if (y->ys_keyword == Y_LEAF || y->ys_keyword == Y_LEAF_LIST){ - if (db_set(filename, xk, val, val?strlen(val)+1:0) < 0) - goto done; - } - else - if (db_set(filename, xk, NULL, 0) < 0) - goto done; - break; - case OP_DELETE: - if ((exists = db_exists(filename, xk)) < 0) - goto done; - if (exists == 0){ - clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); - goto done; - } - case OP_REMOVE: - /* Read in complete database (this can be optimized) */ - if ((crx = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - cprintf(crx, "^%s.*$", xk); - if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0) - goto done; - for (i = 0; i < npairs; i++) { - if (db_del(filename, pairs[i].dp_key) < 0) - goto done; - } - break; - default: - break; - } - retval = 0; - done: - if (filename) - free(filename); - if (ckey) - cbuf_free(ckey); - if (csubkey) - cbuf_free(csubkey); - if (crx) - cbuf_free(crx); - if (cvk) - cvec_free(cvk); - if (vec) - free(vec); - if (valvec) - free(valvec); - - unchunk_group(__FUNCTION__); - return retval; -} - -/*! Modify database provided an xml tree, a restconf api_path and an operation - * - * @param[in] h CLICON handle - * @param[in] db running or candidate - * @param[in] op OP_MERGE: just add it. - * OP_REPLACE: first delete whole database - * OP_NONE: operation attribute in xml determines operation - * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) - * @param[in] xt xml-tree. Top-level symbol is dummy - * @retval 0 OK - * @retval -1 Error - * example: - * container top { - * list list1 { - * key "key1 key2 key3"; - * is referenced as - * /restconf/data/top/list1=a,,foo - * @see xmldb_put - */ -static int -xmldb_put_restconf_api_path(clicon_handle h, - char *db, - enum operation_type op, - char *api_path, - cxobj *xt) -{ - int retval = -1; - yang_stmt *y = NULL; - yang_stmt *ykey; - char **vec = NULL; - int nvec; - int i; - char *name; - cg_var *cvi; - cvec *cvk = NULL; /* vector of index keys */ - char *val2; - cbuf *ckey=NULL; /* partial keys */ - cbuf *csubkey=NULL; /* partial keys */ - cbuf *crx=NULL; /* partial keys */ - char *keyname; - int exists; - int npairs; - struct db_pair *pairs; - yang_spec *yspec; - yang_stmt *ys; - char *filename = NULL; - char *key; - char *keys; - - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &filename) < 0) - goto done; - if (api_path == NULL || *api_path!='/'){ - clicon_err(OE_DB, 0, "Invalid api path: %s", api_path); - goto done; - } - if ((ckey = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - if ((csubkey = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL) - goto done; - if (nvec < 2){ - clicon_err(OE_XML, 0, "Malformed key: %s", api_path); - goto done; - } - i = 1; - while (iys_keyword == Y_LEAF && - y->ys_parent->yn_keyword == Y_LIST && - yang_key_match(y->ys_parent, y->ys_argument)) - /* Special rule if key, dont write last key-name, rm whole*/; - else - cprintf(ckey, "/%s", name); - i++; - switch (y->ys_keyword){ - case Y_LEAF_LIST: - /* For leaf-list 'keys' is value, see 3.5.1 in restconf draft */ - val2 = keys; - cprintf(ckey, "/%s", keys); - break; - case Y_LIST: - if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, y->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - cvi = NULL; - /* Iterate over individual yang keys */ - while ((cvi = cvec_each(cvk, cvi)) != NULL) { - keyname = cv_string_get(cvi); - // val2 = vec[i++]; /* No */ - val2 = keys; - if (i>nvec){ /* XXX >= ? */ - clicon_err(OE_XML, errno, "List %s without argument", name); - goto done; - } - cprintf(ckey, "=%s", val2); - cbuf_reset(csubkey); - cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); - if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) - if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0) - goto done; - } - if (cvk){ - cvec_free(cvk); - cvk = NULL; - } - break; - default: - if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) - if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0) - goto done; - break; - } - } - key = cbuf_get(ckey); - /* final key */ - switch (op){ - case OP_CREATE: - if ((exists = db_exists(filename, key)) < 0) - goto done; - if (exists == 1){ - clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", key); - goto done; - } - case OP_MERGE: - case OP_REPLACE: - if (xt==NULL){ - clicon_err(OE_DB, 0, "%s: no xml when yang node %s required", - __FUNCTION__, y->ys_argument); - goto done; - } - if ((ys = yang_find_syntax((yang_node*)y, xml_name(xt))) == NULL){ - clicon_err(OE_DB, 0, "%s: child %s not found under node %s", - __FUNCTION__, xml_name(xt), y->ys_argument); - goto done; - } - y = ys; - if (put(filename, xt, y, op, key) < 0) - goto done; - break; - case OP_DELETE: - if ((exists = db_exists(filename, key)) < 0) - goto done; - if (exists == 0){ - clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", key); - goto done; - } - case OP_REMOVE: - /* Read in complete database (this can be optimized) */ - if ((crx = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, errno, "cbuf_new"); - goto done; - } - cprintf(crx, "^%s.*$", key); - if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0) - goto done; - for (i = 0; i < npairs; i++) { - if (db_del(filename, pairs[i].dp_key) < 0) - goto done; - } - break; - default: - break; - } - retval = 0; - done: - // clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (filename) - free(filename); - if (ckey) - cbuf_free(ckey); - if (csubkey) - cbuf_free(csubkey); - if (crx) - cbuf_free(crx); - if (cvk) - cvec_free(cvk); - if (vec) - free(vec); - unchunk_group(__FUNCTION__); - return retval; -} - -/*! Modify database provided an xml tree and an operation - * - * @param[in] h CLICON handle - * @param[in] db running or candidate - * @param[in] xt xml-tree. Top-level symbol is dummy - * @param[in] op OP_MERGE: just add it. - * OP_REPLACE: first delete whole database - * OP_NONE: operation attribute in xml determines operation - * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) - * @retval 0 OK - * @retval -1 Error - * The xml may contain the "operation" attribute which defines the operation. - * @code - * cxobj *xt; - * if (clicon_xml_parse_str("17", &xt) < 0) - * err; - * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) - * err; - * @endcode - * @see xmldb_put_xkey for single key - */ -int -xmldb_put(clicon_handle h, - char *db, - enum operation_type op, - char *api_path, - cxobj *xt) -{ - int retval = -1; - cxobj *x = NULL; - yang_stmt *ys; - yang_spec *yspec; - char *dbfilename = NULL; - - if (xml_child_nr(xt)==0 || xml_body(xt)!= NULL) - return xmldb_put_xkey(h, db, op, api_path, xml_body(xt)); - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &dbfilename) < 0) - goto done; - if (op == OP_REPLACE){ - if (db_delete(dbfilename) < 0) - goto done; - if (db_init(dbfilename) < 0) - goto done; - } - while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ - if (api_path && strlen(api_path)){ - if (xmldb_put_restconf_api_path(h, db, op, api_path, x) < 0) - goto done; - continue; - } - if ((ys = yang_find_topnode(yspec, xml_name(x))) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x)); - goto done; - } - if (put(dbfilename, /* database name */ - x, /* xml root node */ - ys, /* yang statement of xml node */ - op, /* operation, eg merge/delete */ - "" /* aggregate xml key */ - ) < 0) - goto done; - } - retval = 0; - done: - if (dbfilename) - free(dbfilename); - return retval; -} - - -/*! Raw dump of database, just keys and values, no xml interpretation - * @param[in] f File - * @param[in] dbfile File-name of database. This is a local file - * @param[in] rxkey Key regexp, eg "^.*$" - * @note This function can only be called locally. - */ -int -xmldb_dump(FILE *f, - char *dbfilename, - char *rxkey) -{ - int retval = -1; - int npairs; - struct db_pair *pairs; - - /* Default is match all */ - if (rxkey == NULL) - rxkey = "^.*$"; - - /* Get all keys/values for vector */ - if ((npairs = db_regexp(dbfilename, rxkey, __FUNCTION__, &pairs, 0)) < 0) - goto done; - - for (npairs--; npairs >= 0; npairs--) - fprintf(f, "%s %s\n", pairs[npairs].dp_key, - pairs[npairs].dp_val?pairs[npairs].dp_val:""); - retval = 0; - done: - unchunk_group(__FUNCTION__); - return retval; -} - -/*! Copy database from db1 to db2 - * @param[in] h Clicon handle - * @param[in] from Source database copy - * @param[in] to Destination database - * @retval -1 Error - * @retval 0 OK - */ -int -xmldb_copy(clicon_handle h, - char *from, - char *to) -{ - int retval = -1; - char *fromfile = NULL; - char *tofile = NULL; - - /* XXX lock */ - if (db2file(h, from, &fromfile) < 0) - goto done; - if (db2file(h, to, &tofile) < 0) - goto done; - if (clicon_file_copy(fromfile, tofile) < 0) - goto done; - retval = 0; - done: - if (fromfile) - free(fromfile); - if (tofile) - free(tofile); - return retval; -} - -/*! Lock database - * @param[in] h Clicon handle - * @param[in] db Database - * @param[in] pid Process id - * @retval -1 Error - * @retval 0 OK - */ -int -xmldb_lock(clicon_handle h, - char *db, - int pid) -{ - int retval = -1; - - if (db_islocked(db)){ - if (pid != db_islocked(db)){ - clicon_err(OE_DB, 0, "lock failed: locked by %d", db_islocked(db)); - goto done; - } - } - else - db_lock(db, pid); - retval = 0; - done: - return retval; -} - -/*! Unlock database - * @param[in] h Clicon handle - * @param[in] db Database - * @param[in] pid Process id - * @retval -1 Error - * @retval 0 OK +/*! Load a specific plugin, call its init function and add it to plugins list + * If init function fails (not found, wrong version, etc) print a log and dont + * add it. + * @param[in] name Name of plugin + * @param[in] filename Actual filename with path + * @param[out] plugin Plugin data structure for invoking. Dealloc with free */ int -xmldb_unlock(clicon_handle h, - char *db, - int pid) +xmldb_plugin_load(char *filename) +{ + int retval = -1; + char *dlerrcode; + plugin_init_t *initfun; + void *handle = NULL; + char *error; + + dlerror(); /* Clear any existing error */ + if ((handle = dlopen(filename, RTLD_NOW|RTLD_GLOBAL)) == NULL) { + error = (char*)dlerror(); + clicon_err(OE_PLUGIN, errno, "dlopen: %s\n", error ? error : "Unknown error"); + goto done; + } + /* Try v1 */ + initfun = dlsym(handle, XMLDB_PLUGIN_INIT_FN); + if ((dlerrcode = (char*)dlerror()) != NULL) { + clicon_log(LOG_WARNING, "Error when loading init function %s: %s", + XMLDB_PLUGIN_INIT_FN, dlerrcode); + goto fail; + } + if ((_xa_api = initfun(XMLDB_API_VERSION)) == NULL) { + clicon_log(LOG_WARNING, "%s: failed when running init function %s: %s", + filename, XMLDB_PLUGIN_INIT_FN, errno?strerror(errno):""); + goto fail; + } + if (_xa_api->xa_version != XMLDB_API_VERSION){ + clicon_log(LOG_WARNING, "%s: Unexpected plugin version number: %d", + filename, _xa_api->xa_version); + goto fail; + } + if (_xa_api->xa_magic != XMLDB_API_MAGIC){ + clicon_log(LOG_WARNING, "%s: Wrong plugin magic number: %x", + filename, _xa_api->xa_magic); + goto fail; + } + clicon_log(LOG_WARNING, "xmldb plugin %s loaded", filename); + retval = 0; + done: + if (retval < 0 && handle) + dlclose(handle); + return retval; + fail: /* plugin load failed, continue */ + retval = 0; + goto done; +} + +int +xmldb_get(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen) { int retval = -1; - int pid1; - pid1 = db_islocked(db); - if (pid1){ - if (pid == pid1) - db_unlock(db); - else{ - clicon_err(OE_DB, 0, "unlock failed: locked by %d", pid1); - goto done; - } + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; } + if (_xa_api->xa_get_fn && + _xa_api->xa_get_fn(h, db, xpath, xtop, xvec, xlen) < 0) + goto done; + retval = 0; + done: + return retval; +} + +int +xmldb_put(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_put_fn && + _xa_api->xa_put_fn(h, db, op, api_path, xt) < 0) + goto done; retval = 0; done: return retval; } -/*! Unlock all databases locked by pid (eg process dies) - */ int -xmldb_unlock_all(clicon_handle h, - int pid) +xmldb_dump(FILE *f, char *dbfilename, char *rxkey) { - return db_unlock_all(pid); -} + int retval = -1; -/*! Check if database is locked - * @param[in] h Clicon handle - * @param[in] db Database - * @retval -1 Error - * @retval 0 Not locked - * @retval >0 Id of locker - */ -int -xmldb_islocked(clicon_handle h, - char *db) -{ - return db_islocked(db); -} - -/*! Check if db exists - * @param[in] h Clicon handle - * @param[in] db Database - * @retval -1 Error - * @retval 0 No it does not exist - * @retval 1 Yes it exists - */ -int -xmldb_exists(clicon_handle h, - char *db) -{ - int retval = -1; - char *filename = NULL; - struct stat sb; - - if (db2file(h, db, &filename) < 0) + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; - if (lstat(filename, &sb) < 0) - retval = 0; - else - retval = 1; - done: - if (filename) - free(filename); - return retval; -} - -/*! Delete database. Remove file - * @param[in] h Clicon handle - * @param[in] db Database - * @retval -1 Error - * @retval 0 OK - */ -int -xmldb_delete(clicon_handle h, - char *db) -{ - int retval = -1; - char *filename = NULL; - - if (db2file(h, db, &filename) < 0) - goto done; - if (db_delete(filename) < 0) + } + if (_xa_api->xa_dump_fn && + _xa_api->xa_dump_fn(f, dbfilename, rxkey) < 0) goto done; retval = 0; done: - if (filename) - free(filename); return retval; } -/*! Initialize database - * @param[in] h Clicon handle - * @param[in] db Database - * @retval 0 OK - * @retval -1 Error - */ int -xmldb_init(clicon_handle h, - char *db) +xmldb_copy(clicon_handle h, char *from, char *to) { - int retval = -1; - char *filename = NULL; + int retval = -1; - if (db2file(h, db, &filename) < 0) + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; - if (db_init(filename) < 0) + } + if (_xa_api->xa_copy_fn && + _xa_api->xa_copy_fn(h, from, to) < 0) goto done; retval = 0; done: - if (filename) - free(filename); return retval; } - -#if 0 /* Test program */ -/* - * Turn this on to get an xpath test program - * Usage: clicon_xpath [] - * read xml from input - * Example compile: - gcc -g -o xmldb -I. -I../clixon ./clixon_xmldb.c -lclixon -lcligen -*/ - -static int -usage(char *argv0) +int +xmldb_lock(clicon_handle h, char *db, int pid) { - fprintf(stderr, "usage:\n%s\tget []\t\txml on stdin\n", argv0); - fprintf(stderr, "\tput set|merge|delete\txml to stdout\n"); - exit(0); + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_lock_fn && + _xa_api->xa_lock_fn(h, db, pid) < 0) + goto done; + retval = 0; + done: + return retval; +} + +int +xmldb_unlock(clicon_handle h, char *db, int pid) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_unlock_fn && + _xa_api->xa_unlock_fn(h, db, pid) < 0) + goto done; + retval = 0; + done: + return retval; } int -main(int argc, char **argv) +xmldb_unlock_all(clicon_handle h, int pid) { - cxobj *xt; - cxobj *xn; - char *xpath; - enum operation_type op; - char *cmd; - char *db; - char *yangdir; - char *yangmod; - yang_spec *yspec = NULL; - clicon_handle h; + int retval = -1; - if ((h = clicon_handle_init()) == NULL) - goto done; - clicon_log_init("xmldb", LOG_DEBUG, CLICON_LOG_STDERR); - if (argc < 4){ - usage(argv[0]); + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - cmd = argv[1]; - db = argv[2]; - yangdir = argv[3]; - yangmod = argv[4]; - db_init(db); - if ((yspec = yspec_new()) == NULL) - goto done - if (yang_parse(h, yangdir, yangmod, NULL, yspec) < 0) + if (_xa_api->xa_unlock_all_fn && + _xa_api->xa_unlock_all_fn(h, pid) < 0) goto done; - if (strcmp(cmd, "get")==0){ - if (argc < 5) - usage(argv[0]); - xpath = argc>5?argv[5]:NULL; - if (xmldb_get(h, db, xpath, &xt, NULL, NULL) < 0) - goto done; - clicon_xml2file(stdout, xt, 0, 1); - } - else - if (strcmp(cmd, "put")==0){ - if (argc != 6) - usage(argv[0]); - if (clicon_xml_parse_file(0, &xt, "") < 0) - goto done; - if (xml_rootchild(xt, 0, &xn) < 0) - goto done; - if (strcmp(argv[5], "set") == 0) - op = OP_REPLACE; - else - if (strcmp(argv[4], "merge") == 0) - op = OP_MERGE; - else if (strcmp(argv[5], "delete") == 0) - op = OP_REMOVE; - else - usage(argv[0]); - if (xmldb_put(h, db, op, NULL, xn) < 0) - goto done; - } - else - usage(argv[0]); - printf("\n"); + retval = 0; done: - return 0; + return retval; } -#endif /* Test program */ +int +xmldb_islocked(clicon_handle h, char *db) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_islocked_fn && + _xa_api->xa_islocked_fn(h, db) < 0) + goto done; + retval = 0; + done: + return retval; +} + +int +xmldb_exists(clicon_handle h, char *db) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_exists_fn && + _xa_api->xa_exists_fn(h, db) < 0) + goto done; + retval = 0; + done: + return retval; +} + +int +xmldb_delete(clicon_handle h, char *db) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_delete_fn && + _xa_api->xa_delete_fn(h, db) < 0) + goto done; + retval = 0; + done: + return retval; +} + +int +xmldb_init(clicon_handle h, char *db) +{ + int retval = -1; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_init_fn && + _xa_api->xa_init_fn(h, db) < 0) + goto done; + retval = 0; + done: + return retval; +} diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 3379beb9..563cba7e 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -66,6 +66,7 @@ #include #include #include +#include /* cligen */ #include @@ -89,9 +90,6 @@ /* Something to do with reverse engineering of junos syntax? */ #undef SPECIAL_TREATMENT_OF_NAME - - - /* * A node is a leaf if it contains a body. */ @@ -791,3 +789,288 @@ xml_diff(yang_spec *yspec, done: return retval; } + +/*! Construct an xml key format from yang statement using wildcards for keys + * Recursively construct it to the top. + * Example: + * yang: container a -> list b -> key c -> leaf d + * xpath: /a/b/%s/d + * @param[in] ys Yang statement + * @param[in] inclkey If inclkey then include key leaf (eg last leaf d in ex) + * @param[out] cbuf keyfmt + */ +static int +yang2xmlkeyfmt_1(yang_stmt *ys, + int inclkey, + cbuf *cb) +{ + yang_node *yp; /* parent */ + yang_stmt *ykey; + int i; + cvec *cvk = NULL; /* vector of index keys */ + int retval = -1; + + yp = ys->ys_parent; + if (yp != NULL && + yp->yn_keyword != Y_MODULE && + yp->yn_keyword != Y_SUBMODULE){ + if (yang2xmlkeyfmt_1((yang_stmt *)yp, 1, cb) < 0) + goto done; + } + if (inclkey){ + if (ys->ys_keyword != Y_CHOICE && ys->ys_keyword != Y_CASE) + cprintf(cb, "/%s", ys->ys_argument); + } + else{ + if (ys->ys_keyword == Y_LEAF && yp && yp->yn_keyword == Y_LIST){ + if (yang_key_match(yp, ys->ys_argument) == 0) + cprintf(cb, "/%s", ys->ys_argument); /* Not if leaf and key */ + } + else + if (ys->ys_keyword != Y_CHOICE && ys->ys_keyword != Y_CASE) + cprintf(cb, "/%s", ys->ys_argument); + } + + switch (ys->ys_keyword){ + case Y_LIST: + if ((ykey = yang_find((yang_node*)ys, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, ys->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + if (cvec_len(cvk)) + cprintf(cb, "="); + /* Iterate over individual keys */ + for (i=0; i list b -> key c -> leaf d + * xpath: /a/b=%s/d + * @param[in] ys Yang statement + * @param[in] inclkey If !inclkey then dont include key leaf + * @param[out] xkfmt XML key format. Needs to be freed after use. + */ +int +yang2xmlkeyfmt(yang_stmt *ys, + int inclkey, + char **xkfmt) +{ + int retval = -1; + cbuf *cb = NULL; + + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + if (yang2xmlkeyfmt_1(ys, inclkey, cb) < 0) + goto done; + if ((*xkfmt = strdup(cbuf_get(cb))) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + + +/*! Transform an xml key format and a vector of values to an XML key + * Used for actual key, eg in clicon_rpc_change(), xmldb_put_xkey() + * Example: + * xmlkeyfmt: /aaa/%s + * cvv: key=17 + * xmlkey: /aaa/17 + * @param[in] xkfmt XML key format, eg /aaa/%s + * @param[in] cvv cligen variable vector, one for every wildchar in xkfmt + * @param[out] xk XML key, eg /aaa/17. Free after use + * @note first and last elements of cvv are not used,.. + * @see cli_dbxml where this function is called + */ +int +xmlkeyfmt2key(char *xkfmt, + cvec *cvv, + char **xk) +{ + int retval = -1; + char c; + int esc=0; + cbuf *cb = NULL; + int i; + int j; + char *str; + char *strenc=NULL; + + + /* Sanity check */ +#if 1 + j = 0; /* Count % */ + for (i=0; i Date: Sun, 9 Apr 2017 22:53:48 +0200 Subject: [PATCH 02/24] Created xmldb plugin api --- CHANGELOG | 4 +- apps/backend/backend_main.c | 3 - apps/backend/backend_plugin.c | 65 +++--- apps/cli/cli_common.c | 13 +- apps/cli/cli_handle.c | 10 +- apps/cli/cli_main.c | 14 +- apps/cli/cli_plugin.c | 202 +++++++----------- apps/cli/cli_plugin.h | 1 - apps/cli/cli_show.c | 14 +- apps/netconf/netconf_plugin.c | 30 ++- apps/restconf/restconf_lib.c | 35 ++- apps/restconf/restconf_main.c | 1 - apps/restconf/restconf_methods.c | 1 - clixon.conf.cpp.cpp | 7 +- configure | 30 --- configure.ac | 18 -- datastore/keyvalue/Makefile.in | 2 +- .../src => datastore/keyvalue}/clixon_chunk.c | 0 .../keyvalue}/clixon_chunk.h | 0 datastore/keyvalue/clixon_keyvalue.c | 141 ++++-------- doc/Doxyfile | 2 +- doc/Doxyfile.graphs | 2 +- include/clixon_config.h.in | 4 - lib/clixon/clixon.h | 91 ++++++++ lib/clixon/clixon_file.h | 4 +- lib/clixon/clixon_options.h | 1 - lib/src/Makefile.in | 3 +- lib/src/clixon_err.c | 14 +- lib/src/clixon_file.c | 69 ++---- lib/src/clixon_options.c | 12 -- lib/src/clixon_proc.c | 1 - lib/src/clixon_proto.c | 8 +- lib/src/clixon_proto_client.c | 1 - lib/src/clixon_string.c | 1 - lib/src/clixon_xml.c | 1 - lib/src/clixon_xml_db.c | 162 +++++++++++--- lib/src/clixon_xml_map.c | 14 +- lib/src/clixon_yang.c | 14 +- lib/src/clixon_yang_type.c | 1 - 39 files changed, 492 insertions(+), 504 deletions(-) rename {lib/src => datastore/keyvalue}/clixon_chunk.c (100%) rename {lib/clixon => datastore/keyvalue}/clixon_chunk.h (100%) create mode 100644 lib/clixon/clixon.h diff --git a/CHANGELOG b/CHANGELOG index 450f5b6c..48032069 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,7 +29,9 @@ # # ***** END LICENSE BLOCK ***** -- Ongoiing: xmldb datastore plugin framework +- Moved qdbm, chunk and xmldb to datastore keyvalue directories + Created xmldb plugin api + Removed all other clixon dependency on chunk code - cli_copy_config added as generic cli command - cli_show_config added as generic cli command diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index fd23e266..1b832fe9 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -95,8 +95,6 @@ config_terminate(clicon_handle h) event_exit(); clicon_log_register_callback(NULL, NULL); clicon_debug(1, "%s done", __FUNCTION__); - if (debug) - chunk_check(stderr, NULL); return 0; } @@ -207,7 +205,6 @@ done: xml_free(xt); if (fd != -1) close(fd); - unchunk_group(__FUNCTION__); return retval; } diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 3477d5cb..4889e6b5 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -178,15 +178,13 @@ plugin_unload(clicon_handle h, * @param[in] h Clicon handle * @param[in] file The plugin (.so) to load * @param[in] dlflags Arguments to dlopen(3) - * @param[in] label Chunk label * @retval plugin Plugin struct * @retval NULL Error */ static struct plugin * plugin_load (clicon_handle h, char *file, - int dlflags, - const char *label) + int dlflags) { char *error; void *handle; @@ -215,7 +213,7 @@ plugin_load (clicon_handle h, return NULL; } - if ((new = chunk(sizeof(*new), label)) == NULL) { + if ((new = malloc(sizeof(*new))) == NULL) { clicon_err(OE_UNIX, errno, "dhunk: %s", strerror(errno)); dlclose(handle); return NULL; @@ -319,8 +317,8 @@ plugin_append(struct plugin *p) { struct plugin *new; - if ((new = rechunk(plugins, (nplugins+1) * sizeof (*p), NULL)) == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((new = realloc(plugins, (nplugins+1) * sizeof (*p))) == NULL) { + clicon_err(OE_UNIX, errno, "realloc"); return -1; } @@ -348,11 +346,11 @@ config_plugin_load_dir(clicon_handle h, int np = 0; int ndp; struct stat st; - char *filename; - struct dirent *dp; + char filename[MAXPATHLEN]; + struct dirent *dp = NULL; struct plugin *new; struct plugin *p = NULL; - char *master; + char master[MAXPATHLEN]; char *master_plugin; /* Format master plugin path */ @@ -360,50 +358,39 @@ config_plugin_load_dir(clicon_handle h, clicon_err(OE_PLUGIN, 0, "clicon_master_plugin option not set"); goto quit; } - master = chunk_sprintf(__FUNCTION__, "%s.so", master_plugin); - if (master == NULL) { - clicon_err(OE_PLUGIN, errno, "chunk_sprintf master plugin"); - goto quit; - } + snprintf(master, MAXPATHLEN-1, "%s.so", master_plugin); /* Allocate plugin group object */ /* Get plugin objects names from plugin directory */ - if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG, __FUNCTION__))<0) + if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG))<0) goto quit; /* reset num plugins */ np = 0; /* Master plugin must be loaded first if it exists. */ - filename = chunk_sprintf(__FUNCTION__, "%s/%s", dir, master); - if (filename == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); - goto quit; - } + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, master); if (stat(filename, &st) == 0) { clicon_debug(1, "Loading master plugin '%.*s' ...", (int)strlen(filename), filename); - new = plugin_load(h, filename, RTLD_NOW|RTLD_GLOBAL, __FUNCTION__); + new = plugin_load(h, filename, RTLD_NOW|RTLD_GLOBAL); if (new == NULL) goto quit; if (plugin_append(new) < 0) goto quit; } - /* Now load the rest */ + /* Now load the rest. Note plugins is the global variable */ for (i = 0; i < ndp; i++) { if (strcmp(dp[i].d_name, master) == 0) continue; /* Skip master now */ - filename = chunk_sprintf(__FUNCTION__, "%s/%s", dir, dp[i].d_name); + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dp[i].d_name); clicon_debug(1, "Loading plugin '%.*s' ...", (int)strlen(filename), filename); - if (filename == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); - goto quit; - } - new = plugin_load (h, filename, RTLD_NOW, __FUNCTION__); + new = plugin_load(h, filename, RTLD_NOW); if (new == NULL) goto quit; + /* Append to 'plugins' */ if (plugin_append(new) < 0) goto quit; } @@ -413,13 +400,19 @@ config_plugin_load_dir(clicon_handle h, quit: if (retval != 0) { - if (p) { - while (--np >= 0) - plugin_unload (h, &p[np]); - unchunk(p); + /* XXX p is always NULL */ + if (plugins) { + while (--np >= 0){ + if ((p = &plugins[np]) == NULL) + continue; + plugin_unload(h, p); + free(p); + } + free(plugins); } } - unchunk_group(__FUNCTION__); + if (dp) + free(dp); return retval; } @@ -464,8 +457,10 @@ plugin_finish(clicon_handle h) p = &plugins[i]; plugin_unload(h, p); } - if (plugins) - unchunk(plugins); + if (plugins){ + free(plugins); + plugins = NULL; + } nplugins = 0; return 0; } diff --git a/apps/cli/cli_common.c b/apps/cli/cli_common.c index 92882d5b..847f1d7f 100644 --- a/apps/cli/cli_common.c +++ b/apps/cli/cli_common.c @@ -94,17 +94,19 @@ cli_notification_register(clicon_handle h, void *arg) { int retval = -1; - char *logname; + char *logname = NULL; void *p; int s; clicon_hash_t *cdat = clicon_data(h); size_t len; int s_exist = -1; - if ((logname = chunk_sprintf(__FUNCTION__, "log_socket_%s", stream)) == NULL){ - clicon_err(OE_PLUGIN, errno, "%s: chunk_sprintf", __FUNCTION__); + len = strlen("log_socket_") + strlen(stream) + 1; + if ((logname = malloc(len)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); goto done; - } + } + snprintf(logname, len, "log_socket_%s", stream); if ((p = hash_value(cdat, logname, &len)) != NULL) s_exist = *(int*)p; @@ -132,7 +134,8 @@ cli_notification_register(clicon_handle h, } retval = 0; done: - unchunk_group(__FUNCTION__); + if (logname) + free(logname); return retval; } diff --git a/apps/cli/cli_handle.c b/apps/cli/cli_handle.c index 7090c304..62c96c11 100644 --- a/apps/cli/cli_handle.c +++ b/apps/cli/cli_handle.c @@ -112,21 +112,23 @@ cli_handle_init(void) return h; } -/* - * cli_handle_exit - * frees clicon handle +/*! Free clicon handle */ int cli_handle_exit(clicon_handle h) { cligen_handle ch = cligen(h); + struct cli_handle *cl = handle(h); + if (cl->cl_stx) + free(cl->cl_stx); clicon_handle_exit(h); /* frees h and options */ + cligen_exit(ch); + return 0; } - /*---------------------------------------------------------- * cli-specific handle access functions *----------------------------------------------------------*/ diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 23e179ce..f8c2a9c7 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -174,7 +174,8 @@ main(int argc, char **argv) int printgen = 0; int logclisyntax = 0; int help = 0; - char *treename; + char *treename = NULL; + int len; int logdst = CLICON_LOG_STDERR; char *restarg = NULL; /* what remains after options */ @@ -343,8 +344,14 @@ main(int argc, char **argv) if (yang2cli(h, yspec, &pt, clicon_cli_genmodel_type(h)) < 0) goto done; - treename = chunk_sprintf(__FUNCTION__, "datamodel:%s", clicon_dbspec_name(h)); + len = strlen("datamodel:") + strlen(clicon_dbspec_name(h)) + 1; + if ((treename = malloc(len)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + snprintf(treename, len, "datamodel:%s", clicon_dbspec_name(h)); cli_tree_add(h, treename, pt); + if (printgen) cligen_print(stdout, pt, 1); } @@ -400,9 +407,10 @@ main(int argc, char **argv) if (!once) cli_interactive(h); done: + if (treename) + free(treename); if (restarg) free(restarg); - unchunk_group(__FUNCTION__); // Gets in your face if we log on stderr clicon_log_init(__PROGRAM__, LOG_INFO, 0); /* Log on syslog no stderr */ clicon_log(LOG_NOTICE, "%s: %u Terminated\n", __PROGRAM__, getpid()); diff --git a/apps/cli/cli_plugin.c b/apps/cli/cli_plugin.c index cbb68cbd..fdbe8408 100644 --- a/apps/cli/cli_plugin.c +++ b/apps/cli/cli_plugin.c @@ -81,8 +81,7 @@ * */ -/* - * Find syntax mode named 'mode'. Create if specified +/*! Find syntax mode named 'mode'. Create if specified */ static cli_syntaxmode_t * syntax_mode_find(cli_syntax_t *stx, const char *mode, int create) @@ -101,8 +100,8 @@ syntax_mode_find(cli_syntax_t *stx, const char *mode, int create) if (create == 0) return NULL; - if ((m = chunk(sizeof(cli_syntaxmode_t), stx->stx_cnklbl)) == NULL) { - perror("chunk"); + if ((m = malloc(sizeof(cli_syntaxmode_t))) == NULL) { + perror("malloc"); return NULL; } memset (m, 0, sizeof (*m)); @@ -195,7 +194,7 @@ static int syntax_unload(clicon_handle h) { struct cli_plugin *p; - cli_syntax_t *stx = cli_syntax(h); + cli_syntax_t *stx = cli_syntax(h); if (stx == NULL) return 0; @@ -205,14 +204,16 @@ syntax_unload(clicon_handle h) plugin_unload(h, p->cp_handle); clicon_debug(1, "DEBUG: Plugin '%s' unloaded.", p->cp_name); DELQ(stx->stx_plugins, stx->stx_plugins, struct cli_plugin *); + if (stx->stx_plugins) + free(stx->stx_plugins); stx->stx_nplugins--; } while (stx->stx_nmodes > 0) { DELQ(stx->stx_modes, stx->stx_modes, cli_syntaxmode_t *); + if (stx->stx_modes) + free(stx->stx_modes); stx->stx_nmodes--; } - - unchunk_group(stx->stx_cnklbl); return 0; } @@ -265,12 +266,14 @@ clixon_str2fn(char *name, return NULL; } -/* - * Load a dynamic plugin object and call it's init-function +/*! Load a dynamic plugin object and call it's init-function * Note 'file' may be destructively modified + * @retval plugin-handle should be freed after use */ static plghndl_t -cli_plugin_load (clicon_handle h, char *file, int dlflags, const char *cnklbl) +cli_plugin_load(clicon_handle h, + char *file, + int dlflags) { char *error; char *name; @@ -292,8 +295,8 @@ cli_plugin_load (clicon_handle h, char *file, int dlflags, const char *cnklbl) } } - if ((cp = chunk(sizeof (struct cli_plugin), cnklbl)) == NULL) { - perror("chunk"); + if ((cp = malloc(sizeof (struct cli_plugin))) == NULL) { + perror("malloc"); goto quit; } memset (cp, 0, sizeof(*cp)); @@ -323,7 +326,7 @@ cli_load_syntax(clicon_handle h, const char *filename, const char *clispec_dir) parse_tree pt = {0,}; int retval = -1; FILE *f; - char *filepath; + char filepath[MAXPATHLEN]; cvec *vr = NULL; char *prompt = NULL; char **vec = NULL; @@ -331,12 +334,7 @@ cli_load_syntax(clicon_handle h, const char *filename, const char *clispec_dir) char *plgnam; struct cli_plugin *p; - if ((filepath = chunk_sprintf(__FUNCTION__, "%s/%s", - clispec_dir, - filename)) == NULL){ - clicon_err(OE_PLUGIN, errno, "chunk"); - goto done; - } + snprintf(filepath, MAXPATHLEN-1, "%s/%s", clispec_dir, filename); if ((vr = cvec_new(0)) == NULL){ clicon_err(OE_PLUGIN, errno, "cvec_new"); goto done; @@ -403,7 +401,6 @@ done: cvec_free(vr); if (vec) free(vec); - unchunk_group(__FUNCTION__); return retval; } @@ -415,10 +412,10 @@ cli_plugin_load_dir(clicon_handle h, char *dir, cli_syntax_t *stx) { int i; int ndp; - struct dirent *dp; - char *file; + struct dirent *dp = NULL; char *master_plugin; - char *master; + char master[MAXPATHLEN]; + char filename[MAXPATHLEN]; struct cli_plugin *cp; struct stat st; int retval = -1; @@ -429,24 +426,18 @@ cli_plugin_load_dir(clicon_handle h, char *dir, cli_syntax_t *stx) clicon_err(OE_PLUGIN, 0, "clicon_master_plugin option not set"); goto quit; } - if ((master = chunk_sprintf(__FUNCTION__, "%s.so", master_plugin)) == NULL){ - clicon_err(OE_PLUGIN, errno, "chunk_sprintf master plugin"); - goto quit; - } + snprintf(master, MAXPATHLEN-1, "%s.so", master_plugin); + /* Get plugin objects names from plugin directory */ - ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG, __FUNCTION__); + ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG); if (ndp < 0) goto quit; /* Load master plugin first */ - file = chunk_sprintf(__FUNCTION__, "%s/%s", dir, master); - if (file == NULL) { - clicon_err(OE_UNIX, errno, "chunk_sprintf dir"); - goto quit; - } - if (stat(file, &st) == 0) { + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, master); + if (stat(filename, &st) == 0) { clicon_debug(1, "DEBUG: Loading master plugin '%s'", master); - cp = cli_plugin_load(h, file, RTLD_NOW|RTLD_GLOBAL, stx->stx_cnklbl); + cp = cli_plugin_load(h, filename, RTLD_NOW|RTLD_GLOBAL); if (cp == NULL) goto quit; /* Look up certain call-backs in master plugin */ @@ -459,33 +450,25 @@ cli_plugin_load_dir(clicon_handle h, char *dir, cli_syntax_t *stx) INSQ(cp, stx->stx_plugins); stx->stx_nplugins++; } - unchunk (file); /* Load the rest */ for (i = 0; i < ndp; i++) { if (strcmp (dp[i].d_name, master) == 0) continue; /* Skip master now */ - file = chunk_sprintf(__FUNCTION__, "%s/%s", dir, dp[i].d_name); - if (file == NULL) { - clicon_err(OE_UNIX, errno, "chunk_sprintf dir"); - goto quit; - } + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dp[i].d_name); clicon_debug(1, "DEBUG: Loading plugin '%s'", dp[i].d_name); - if ((cp = cli_plugin_load (h, file, RTLD_NOW, stx->stx_cnklbl)) == NULL) + if ((cp = cli_plugin_load (h, filename, RTLD_NOW)) == NULL) goto quit; INSQ(cp, stx->stx_plugins); stx->stx_nplugins++; - unchunk (file); } - if (dp) - unchunk(dp); retval = 0; quit: - unchunk_group(__FUNCTION__); - + if (dp) + free(dp); return retval; } @@ -501,8 +484,7 @@ cli_syntax_load (clicon_handle h) char *clispec_dir = NULL; int ndp; int i; - char *cnklbl = "__CLICON_CLI_SYNTAX_CNK_LABEL__"; - struct dirent *dp; + struct dirent *dp = NULL; cli_syntax_t *stx; cli_syntaxmode_t *m; @@ -521,13 +503,11 @@ cli_syntax_load (clicon_handle h) } /* Allocate plugin group object */ - if ((stx = chunk(sizeof(*stx), cnklbl)) == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((stx = malloc(sizeof(*stx))) == NULL) { + clicon_err(OE_UNIX, errno, "malloc"); goto quit; } memset (stx, 0, sizeof (*stx)); /* Zero out all */ - /* populate name and chunk label */ - strncpy (stx->stx_cnklbl, cnklbl, sizeof(stx->stx_cnklbl)-1); cli_syntax_set(h, stx); @@ -541,7 +521,7 @@ cli_syntax_load (clicon_handle h) goto quit; /* load syntaxfiles */ - if ((ndp = clicon_file_dirent(clispec_dir, &dp, "(.cli)$", S_IFREG, __FUNCTION__)) < 0) + if ((ndp = clicon_file_dirent(clispec_dir, &dp, "(.cli)$", S_IFREG)) < 0) goto quit; /* Load the rest */ for (i = 0; i < ndp; i++) { @@ -550,9 +530,6 @@ cli_syntax_load (clicon_handle h) if (cli_load_syntax(h, dp[i].d_name, clispec_dir) < 0) goto quit; } - if (dp) - unchunk(dp); - /* Did we successfully load any syntax modes? */ if (stx->stx_nmodes <= 0) { @@ -577,10 +554,10 @@ cli_syntax_load (clicon_handle h) quit: if (retval != 0) { syntax_unload(h); - unchunk_group(cnklbl); cli_syntax_set(h, NULL); } - unchunk_group(__FUNCTION__); + if (dp) + free(dp); return retval; } @@ -663,39 +640,36 @@ clicon_eval(clicon_handle h, char *cmd, cg_obj *match_obj, cvec *vr) } -/* - * clicon_parse - * Given a command string, parse and evaluate the string according to +/*! Given a command string, parse and evaluate. + * Parse and evaluate the string according to * the syntax parse tree of the syntax mode specified by *mode. * If there is no match in the tree for the command, the parse hook * will be called to see if another mode should be evaluated. If a * match is found in another mode, the mode variable is updated to point at * the new mode string. * - * INPUT: - * cmd The command string - * match_obj Pointer to CLIgen match object - * mode A pointer to the mode string pointer - * OUTPUT: - * kr Keyword vector - * vr Variable vector - * RETURNS: - * -2 : on eof (shouldnt happen) - * -1 : In parse error - * >=0 : Number of matches + * @param[in] h Clicon handle + * @param[in] cmd The command string + * @param[in,out] mode A pointer to the mode string pointer + * @param[out] result -2 On eof (shouldnt happen) + * -1 On parse error + * >=0 Number of matches */ int -clicon_parse(clicon_handle h, char *cmd, char **mode, int *result) +clicon_parse(clicon_handle h, + char *cmd, + char **mode, + int *result) { - char *m, *msav; - int res = -1; - int r; - cli_syntax_t *stx; + char *m, *msav; + int res = -1; + int r; + cli_syntax_t *stx = NULL; cli_syntaxmode_t *smode; char *treename; parse_tree *pt; /* Orig */ cg_obj *match_obj; - cvec *vr = NULL; + cvec *cvv = NULL; stx = cli_syntax(h); m = *mode; @@ -719,11 +693,11 @@ clicon_parse(clicon_handle h, char *cmd, char **mode, int *result) fprintf(stderr, "No such parse-tree registered: %s\n", treename); goto done;; } - if ((vr = cvec_new(0)) == NULL){ - fprintf(stderr, "%s: cvec_new: %s\n", __FUNCTION__, strerror(errno)); + if ((cvv = cvec_new(0)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_new"); goto done;; } - res = cliread_parse(cli_cligen(h), cmd, pt, &match_obj, vr); + res = cliread_parse(cli_cligen(h), cmd, pt, &match_obj, cvv); if (res != CG_MATCH) pt_expand_cleanup_1(pt); if (msav){ @@ -755,7 +729,7 @@ clicon_parse(clicon_handle h, char *cmd, char **mode, int *result) *mode = m; cli_set_syntax_mode(h, m); } - if ((r = clicon_eval(h, cmd, match_obj, vr)) < 0) + if ((r = clicon_eval(h, cmd, match_obj, cvv)) < 0) cli_handler_err(stdout); pt_expand_cleanup_1(pt); if (result) @@ -769,8 +743,8 @@ clicon_parse(clicon_handle h, char *cmd, char **mode, int *result) } } done: - if (vr) - cvec_free(vr); + if (cvv) + cvec_free(cvv); return res; } @@ -891,9 +865,7 @@ cli_set_prompt(clicon_handle h, const char *name, const char *prompt) return 0; } -/* - * Format prompt - * XXX: HOST_NAME_MAX from sysconf() +/*! Format prompt */ static int prompt_fmt (char *prompt, size_t plen, char *fmt, ...) @@ -902,65 +874,56 @@ prompt_fmt (char *prompt, size_t plen, char *fmt, ...) char *s = fmt; char hname[1024]; char tty[32]; - char *new; char *tmp; int ret = -1; + cbuf *cb = NULL; + + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } /* Start with empty string */ - if((new = chunk_sprintf(__FUNCTION__, "%s", ""))==NULL) - goto done; - + cprintf(cb, ""); while(*s) { if (*s == '%' && *++s) { switch(*s) { - case 'H': /* Hostname */ if (gethostname (hname, sizeof (hname)) != 0) strncpy(hname, "unknown", sizeof(hname)-1); - if((new = chunk_strncat(new, hname, 0, __FUNCTION__))==NULL) - goto done; + cprintf(cb, "%s", hname); break; - case 'U': /* Username */ tmp = getenv("USER"); - if((new = chunk_strncat(new, (tmp ? tmp : "nobody"), 0, __FUNCTION__))==NULL) - goto done; + cprintf(cb, "%s", tmp?tmp:"nobody"); break; - case 'T': /* TTY */ if(ttyname_r(fileno(stdin), tty, sizeof(tty)-1) < 0) strcpy(tty, "notty"); - if((new = chunk_strncat(new, tty, strlen(tty), __FUNCTION__))==NULL) - goto done; + cprintf(cb, "%s", tty); break; - default: - if((new = chunk_strncat(new, "%", 1, __FUNCTION__))==NULL || - (new = chunk_strncat(new, s, 1, __FUNCTION__))) - goto done; + cprintf(cb, "%%"); + cprintf(cb, "%c", *s); } } - else { - if ((new = chunk_strncat(new, s, 1, __FUNCTION__))==NULL) - goto done; - } + else + cprintf(cb, "%c", *s); s++; } done: - if (new) - fmt = new; + if (cb) + fmt = cbuf_get(cb); va_start(ap, fmt); ret = vsnprintf(prompt, plen, fmt, ap); va_end(ap); - - unchunk_group(__FUNCTION__); - + if (cb) + cbuf_free(cb); return ret; } -/* - * Return a formatted prompt string +/*! Return a formatted prompt string */ char * cli_prompt(char *fmt) @@ -1069,11 +1032,10 @@ cli_ptpop(clicon_handle h, char *mode, char *op) } -/* - * clicon_valcb +/*! Find a cli plugin based on name and resolve a function pointer in it. * Callback from clicon_dbvars_parse() - * Find a cli plugin based on name if given and - * use dlsym to resolve a function pointer in it. + * Find a cli plugin based on name if given and use dlsym to resolve a + * function pointer in it. * Call the resolved function to get the cgv populated */ int diff --git a/apps/cli/cli_plugin.h b/apps/cli/cli_plugin.h index 8823a052..0a74e6a7 100644 --- a/apps/cli/cli_plugin.h +++ b/apps/cli/cli_plugin.h @@ -74,7 +74,6 @@ struct cli_plugin { /* Plugin group object */ typedef struct { - char stx_cnklbl[128]; /* Plugin group name */ int stx_nplugins; /* Number of plugins */ struct cli_plugin *stx_plugins; /* List of plugins */ int stx_nmodes; /* Number of syntax modes */ diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index 392493f3..95df07b8 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -213,15 +213,15 @@ expand_dir(char *dir, mode_t flags, int detail) { - DIR *dirp; + DIR *dirp; struct dirent *dp; - struct stat st; - char *str; - char *cmd; - int len; - int retval = -1; + struct stat st; + char *str; + char *cmd; + int len; + int retval = -1; struct passwd *pw; - char filename[MAXPATHLEN]; + char filename[MAXPATHLEN]; if ((dirp = opendir(dir)) == 0){ fprintf(stderr, "expand_dir: opendir(%s) %s\n", diff --git a/apps/netconf/netconf_plugin.c b/apps/netconf/netconf_plugin.c index ce68c92b..9dc5d434 100644 --- a/apps/netconf/netconf_plugin.c +++ b/apps/netconf/netconf_plugin.c @@ -105,7 +105,7 @@ plugin_unload(clicon_handle h, void *handle) * Note 'file' may be destructively modified */ static plghndl_t -plugin_load (clicon_handle h, char *file, int dlflags, const char *cnklbl) +plugin_load (clicon_handle h, char *file, int dlflags) { char *error; void *handle = NULL; @@ -141,9 +141,9 @@ netconf_plugin_load(clicon_handle h) int retval = -1; char *dir; int ndp; - struct dirent *dp; + struct dirent *dp = NULL; int i; - char *filename; + char filename[MAXPATHLEN]; plghndl_t *handle; if ((dir = clicon_netconf_dir(h)) == NULL){ @@ -152,30 +152,26 @@ netconf_plugin_load(clicon_handle h) } /* Get plugin objects names from plugin directory */ - if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG, __FUNCTION__))<0) + if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG))<0) goto quit; /* Load all plugins */ for (i = 0; i < ndp; i++) { - filename = chunk_sprintf(__FUNCTION__, "%s/%s", dir, dp[i].d_name); + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dp[i].d_name); clicon_debug(1, "DEBUG: Loading plugin '%.*s' ...", (int)strlen(filename), filename); - if (filename == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((handle = plugin_load(h, filename, RTLD_NOW)) == NULL) goto quit; - } - if ((handle = plugin_load (h, filename, RTLD_NOW, __FUNCTION__)) == NULL) - goto quit; - if ((plugins = rechunk(plugins, (nplugins+1) * sizeof (*plugins), NULL)) == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((plugins = realloc(plugins, (nplugins+1) * sizeof (*plugins))) == NULL) { + clicon_err(OE_UNIX, errno, "realloc"); goto quit; } plugins[nplugins++] = handle; - unchunk (filename); } retval = 0; quit: - unchunk_group(__FUNCTION__); + if (dp) + free(dp); return retval; } @@ -194,8 +190,10 @@ netconf_plugin_unload(clicon_handle h) } for (i = 0; i < nplugins; i++) plugin_unload(h, plugins[i]); - if (plugins) - unchunk(plugins); + if (plugins){ + free(plugins); + plugins = NULL; + } nplugins = 0; return 0; } diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index adda4275..9515f4ff 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -33,7 +33,6 @@ */ - #include #include #include @@ -46,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -175,7 +175,7 @@ str2cvec(char *string, s++; cv_name_set(cv, s); cv_string_set(cv, valu); - free(valu); + curl_free(valu); } else{ if (strlen(s)){ @@ -278,8 +278,7 @@ static credentials_t *p_credentials = NULL; /* Credentials callback */ static plghndl_t plugin_load (clicon_handle h, char *file, - int dlflags, - const char *cnklbl) + int dlflags) { char *error; void *handle = NULL; @@ -319,40 +318,36 @@ restconf_plugin_load(clicon_handle h) int retval = -1; char *dir; int ndp; - struct dirent *dp; + struct dirent *dp = NULL; int i; - char *filename; plghndl_t *handle; + char filename[MAXPATHLEN]; if ((dir = clicon_restconf_dir(h)) == NULL){ clicon_err(OE_PLUGIN, 0, "clicon_restconf_dir not defined"); goto quit; } /* Get plugin objects names from plugin directory */ - if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG, __FUNCTION__))<0) + if((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG))<0) goto quit; /* Load all plugins */ for (i = 0; i < ndp; i++) { - filename = chunk_sprintf(__FUNCTION__, "%s/%s", dir, dp[i].d_name); + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dp[i].d_name); clicon_debug(1, "DEBUG: Loading plugin '%.*s' ...", (int)strlen(filename), filename); - if (filename == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((handle = plugin_load(h, filename, RTLD_NOW)) == NULL) goto quit; - } - if ((handle = plugin_load (h, filename, RTLD_NOW, __FUNCTION__)) == NULL) - goto quit; - if ((plugins = rechunk(plugins, (nplugins+1) * sizeof (*plugins), NULL)) == NULL) { - clicon_err(OE_UNIX, errno, "chunk"); + if ((plugins = realloc(plugins, (nplugins+1) * sizeof (*plugins))) == NULL) { + clicon_err(OE_UNIX, errno, "realloc"); goto quit; } plugins[nplugins++] = handle; - unchunk (filename); } retval = 0; quit: - unchunk_group(__FUNCTION__); + if (dp) + free(dp); return retval; } @@ -387,8 +382,10 @@ restconf_plugin_unload(clicon_handle h) for (i = 0; i < nplugins; i++) plugin_unload(h, plugins[i]); - if (plugins) - unchunk(plugins); + if (plugins){ + free(plugins); + plugins = NULL; + } nplugins = 0; return 0; } diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 93936d18..90d0e3e2 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -58,7 +58,6 @@ #include #include #include -#include #include /* cligen */ diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 5cdb1208..8a74a0d6 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -109,7 +109,6 @@ Mapping netconf error-tag -> status code #include #include #include -#include /* cligen */ #include diff --git a/clixon.conf.cpp.cpp b/clixon.conf.cpp.cpp index 57923b95..e6481452 100644 --- a/clixon.conf.cpp.cpp +++ b/clixon.conf.cpp.cpp @@ -73,10 +73,6 @@ CLICON_CLI_DIR libdir/APPNAME/cli # Location of frontend .cli cligen spec files CLICON_CLISPEC_DIR libdir/APPNAME/clispec -# Directory where to save configuration commit history (in XML). Snapshots -# are saved chronologically -CLICON_ARCHIVE_DIR localstatedir/APPNAME/archive - # Enabled uses "startup" configuration on boot CLICON_USE_STARTUP_CONFIG 0 @@ -121,6 +117,9 @@ CLICON_BACKEND_PIDFILE localstatedir/APPNAME/APPNAME.pidfile # Directory where "running", "candidate" and "startup" are placed CLICON_XMLDB_DIR localstatedir/APPNAME +# XMLDB datastore plugin filename (see datastore/ and clixon_xml_db.[ch]) +CLICON_XMLDB_PLUGIN libdir/xmldb/keyvalue.so + # Dont include keys in cvec in cli vars callbacks, ie a & k in 'a k ' ignored # CLICON_CLI_VARONLY 1 diff --git a/configure b/configure index e7a3ac5f..bf0f6ace 100755 --- a/configure +++ b/configure @@ -703,7 +703,6 @@ ac_user_opts=' enable_option_checking with_cligen with_qdbm -enable_keycontent ' ac_precious_vars='build_alias host_alias @@ -1324,12 +1323,6 @@ if test -n "$ac_init_help"; then cat <<\_ACEOF -Optional Features: - --disable-option-checking ignore unrecognized --enable/--with options - --disable-FEATURE do not include FEATURE (same as --enable-FEATURE=no) - --enable-FEATURE[=ARG] include FEATURE [ARG=yes] - --disable-keycontent Disable reverse lookup content keys - Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) @@ -4290,29 +4283,6 @@ else fi -# Check if extra keys inserted for database lists containing content. Eg A.n.foo = 3 -# means A.3 $!a=foo exists - -# Check whether --enable-keycontent was given. -if test "${enable_keycontent+set}" = set; then : - enableval=$enable_keycontent; - if test "$enableval" = no; then - ac_enable_keycontent=no - else - ac_enable_keycontent=yes - fi - -else - ac_enable_keycontent=yes -fi - - - -if test "$ac_enable_keycontent" = "yes"; then - $as_echo "#define DB_KEYCONTENT 1" >>confdefs.h - -fi - ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile doc/Makefile" diff --git a/configure.ac b/configure.ac index c156443f..d8ff3cda 100644 --- a/configure.ac +++ b/configure.ac @@ -159,24 +159,6 @@ AC_CHECK_FUNCS(inet_aton sigaction sigvec strlcpy strsep strndup alphasort versi # Lives in libfcgi-dev AC_CHECK_LIB(fcgi, FCGX_Init,, AC_MSG_ERROR([libfcgi-dev missing])) -# Check if extra keys inserted for database lists containing content. Eg A.n.foo = 3 -# means A.3 $!a=foo exists - -AC_ARG_ENABLE(keycontent, [ --disable-keycontent Disable reverse lookup content keys],[ - if test "$enableval" = no; then - ac_enable_keycontent=no - else - ac_enable_keycontent=yes - fi - ],[ ac_enable_keycontent=yes]) - -AH_TEMPLATE([DB_KEYCONTENT], -[ Check if extra keys inserted for database lists containing content. - Eg A.n.foo = 3 means A.3 $!a=foo exists]) -if test "$ac_enable_keycontent" = "yes"; then - AC_DEFINE(DB_KEYCONTENT) -fi - AH_BOTTOM([#include ]) AC_OUTPUT(Makefile diff --git a/datastore/keyvalue/Makefile.in b/datastore/keyvalue/Makefile.in index 7c79e7f5..0737450e 100644 --- a/datastore/keyvalue/Makefile.in +++ b/datastore/keyvalue/Makefile.in @@ -55,7 +55,7 @@ INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$ PLUGIN = $(DATASTORE).so -SRC = clixon_keyvalue.c clixon_qdb.c +SRC = clixon_keyvalue.c clixon_qdb.c clixon_chunk.c OBJS = $(SRC:.c=.o) diff --git a/lib/src/clixon_chunk.c b/datastore/keyvalue/clixon_chunk.c similarity index 100% rename from lib/src/clixon_chunk.c rename to datastore/keyvalue/clixon_chunk.c diff --git a/lib/clixon/clixon_chunk.h b/datastore/keyvalue/clixon_chunk.h similarity index 100% rename from lib/clixon/clixon_chunk.h rename to datastore/keyvalue/clixon_chunk.h diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index faaf1242..84c31f94 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -119,70 +119,6 @@ static int _running_locked = 0; static int _candidate_locked = 0; static int _startup_locked = 0; -/*! Lock database - * @param[in] db Database name - * @param[in] pid process/session-id - */ -static int -db_lock(char *db, - int pid) -{ - if (strcmp("running", db) == 0) - _running_locked = pid; - else if (strcmp("candidate", db) == 0) - _candidate_locked = pid; - else if (strcmp("startup", db) == 0) - _startup_locked = pid; - clicon_debug(1, "%s: locked by %u", db, pid); - return 0; -} - -/*! Unlock database - * @param[in] db Database name - */ -static int -db_unlock(char *db) -{ - if (strcmp("running", db) == 0) - _running_locked = 0; - else if (strcmp("candidate", db) == 0) - _candidate_locked = 0; - else if (strcmp("startup", db) == 0) - _startup_locked = 0; - return 0; -} - -/*! Unlock all databases locked by pid (eg process dies) - * @param[in] pid process/session-id - */ -static int -db_unlock_all(int pid) -{ - if (_running_locked == pid) - _running_locked = 0; - if (_candidate_locked == pid) - _candidate_locked = 0; - if (_startup_locked == pid) - _startup_locked = 0; - return 0; -} - -/*! returns id of locker - * @retval 0 Not locked - * @retval >0 Id of locker - */ -static int -db_islocked(char *db) -{ - if (strcmp("running", db) == 0) - return (_running_locked); - else if (strcmp("candidate", db) == 0) - return(_candidate_locked); - else if (strcmp("startup", db) == 0) - return(_startup_locked); - return 0; -} - /*! Translate from symbolic database name to actual filename in file-system * @param[in] h Clicon handle * @param[in] db Symbolic database name, eg "candidate", "running" @@ -273,7 +209,7 @@ append_listkeys(cbuf *ckey, else cprintf(ckey, "="); cprintf(ckey, "%s", bodyenc); - free(bodyenc); bodyenc = NULL; + curl_free(bodyenc); bodyenc = NULL; } retval = 0; done: @@ -436,7 +372,7 @@ get(char *dbname, goto done; /* Assume body is created at end of function */ } - free(argdec); + curl_free(argdec); argdec = NULL; break; case Y_LIST: @@ -478,7 +414,7 @@ get(char *dbname, goto done; } cprintf(cb, "[%s=%s]", cv_string_get(cvi), argdec); - free(argdec); + curl_free(argdec); argdec=NULL; } if ((xc = xpath_first(x, cbuf_get(cb))) == NULL){ @@ -502,7 +438,7 @@ get(char *dbname, argdec, keyname) < 0) goto done; - free(argdec); argdec = NULL; + curl_free(argdec); argdec = NULL; } /* while */ } if (cb){ @@ -710,7 +646,7 @@ kv_get(clicon_handle h, int retval = -1; yang_spec *yspec; char *dbname = NULL; - cxobj **xvec; + cxobj **xvec = NULL; size_t xlen; int i; int npairs; @@ -885,7 +821,7 @@ put(char *dbname, if (cbxk) cbuf_free(cbxk); if (bodyenc) - free(bodyenc); + curl_free(bodyenc); return retval; } @@ -1400,7 +1336,6 @@ kv_put(clicon_handle h, return retval; } - /*! Raw dump of database, just keys and values, no xml interpretation * @param[in] f File * @param[in] dbfile File-name of database. This is a local file @@ -1477,19 +1412,14 @@ kv_lock(clicon_handle h, char *db, int pid) { - int retval = -1; - - if (db_islocked(db)){ - if (pid != db_islocked(db)){ - clicon_err(OE_DB, 0, "lock failed: locked by %d", db_islocked(db)); - goto done; - } - } - else - db_lock(db, pid); - retval = 0; - done: - return retval; + if (strcmp("running", db) == 0) + _running_locked = pid; + else if (strcmp("candidate", db) == 0) + _candidate_locked = pid; + else if (strcmp("startup", db) == 0) + _startup_locked = pid; + clicon_debug(1, "%s: locked by %u", db, pid); + return 0; } /*! Unlock database @@ -1498,36 +1428,39 @@ kv_lock(clicon_handle h, * @param[in] pid Process id * @retval -1 Error * @retval 0 OK + * Assume all sanity checks have been made */ int kv_unlock(clicon_handle h, char *db, int pid) { - int retval = -1; - int pid1; - - pid1 = db_islocked(db); - if (pid1){ - if (pid == pid1) - db_unlock(db); - else{ - clicon_err(OE_DB, 0, "unlock failed: locked by %d", pid1); - goto done; - } - } - retval = 0; - done: - return retval; + if (strcmp("running", db) == 0) + _running_locked = 0; + else if (strcmp("candidate", db) == 0) + _candidate_locked = 0; + else if (strcmp("startup", db) == 0) + _startup_locked = 0; + return 0; } /*! Unlock all databases locked by pid (eg process dies) + * @param[in] h Clicon handle + * @param[in] pid Process / Session id + * @retval -1 Error + * @retval 0 Ok */ int kv_unlock_all(clicon_handle h, int pid) { - return db_unlock_all(pid); + if (_running_locked == pid) + _running_locked = 0; + if (_candidate_locked == pid) + _candidate_locked = 0; + if (_startup_locked == pid) + _startup_locked = 0; + return 0; } /*! Check if database is locked @@ -1541,7 +1474,13 @@ int kv_islocked(clicon_handle h, char *db) { - return db_islocked(db); + if (strcmp("running", db) == 0) + return (_running_locked); + else if (strcmp("candidate", db) == 0) + return(_candidate_locked); + else if (strcmp("startup", db) == 0) + return(_startup_locked); + return 0; } /*! Check if db exists diff --git a/doc/Doxyfile b/doc/Doxyfile index fba55c6c..f18d01f3 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -743,7 +743,7 @@ WARN_LOGFILE = # spaces. # Note: If this tag is empty the current directory is searched. -INPUT = ../lib/src/ ../lib/clicon/ ../apps/cli/ ../apps/config/ ../apps/netconf ../apps/dbctrl +INPUT = ../lib/src/ ../lib/clicon/ ../apps/cli/ ../apps/config/ ../apps/netconf ../apps/dbctrl ../datastore # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/doc/Doxyfile.graphs b/doc/Doxyfile.graphs index a264d0fc..7a115a20 100644 --- a/doc/Doxyfile.graphs +++ b/doc/Doxyfile.graphs @@ -743,7 +743,7 @@ WARN_LOGFILE = # spaces. # Note: If this tag is empty the current directory is searched. -INPUT = ../lib/src/ ../lib/clicon/ ../apps/cli/ ../apps/backend/ ../apps/restconf/ ../apps/netconf ../apps/dbctrl +INPUT = ../lib/src/ ../lib/clicon/ ../apps/cli/ ../apps/backend/ ../apps/restconf/ ../apps/netconf ../apps/dbctrl ../datastore # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses diff --git a/include/clixon_config.h.in b/include/clixon_config.h.in index c7651da8..736baf37 100644 --- a/include/clixon_config.h.in +++ b/include/clixon_config.h.in @@ -12,10 +12,6 @@ /* Clixon version string */ #undef CLIXON_VERSION_STRING -/* Check if extra keys inserted for database lists containing content. Eg - A.n.foo = 3 means A.3 $!a=foo exists */ -#undef DB_KEYCONTENT - /* Define to 1 if you have the `alphasort' function. */ #undef HAVE_ALPHASORT diff --git a/lib/clixon/clixon.h b/lib/clixon/clixon.h new file mode 100644 index 00000000..4ccdcab5 --- /dev/null +++ b/lib/clixon/clixon.h @@ -0,0 +1,91 @@ +/* lib/clixon/clixon.h. Generated from clixon.h.in by configure. */ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Meta-include file that includes all sub-files in control-lib + * Note: this include files is for external purposes. Do not include this + * file in clicon lib-routines. + */ + +/* This include file requires the following include file dependencies */ +#include +#include +#include +#include +#include +#include + +/* + * CLIXON version macros + */ + +#define CLIXON_VERSION_STRING "3.2.0" +#define CLIXON_VERSION_MAJOR 3 +#define CLIXON_VERSION_MINOR 2 +#define CLIXON_VERSION_PATCH 0 + +/* + * Use this constant to disable some prototypes that should not be visible outside the lib. + * This is an alternative to use separate internal include files. + */ +#define LIBCLIXON_API 1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * Global variables generated by Makefile + */ +extern const char CLIXON_BUILDSTR[]; +extern const char CLIXON_VERSION[]; diff --git a/lib/clixon/clixon_file.h b/lib/clixon/clixon_file.h index 7a57119b..5965f62b 100644 --- a/lib/clixon/clixon_file.h +++ b/lib/clixon/clixon_file.h @@ -38,9 +38,7 @@ int clicon_file_dirent(const char *dir, struct dirent **ent, - const char *regexp, mode_t type, const char *label); - -char *clicon_tmpfile(const char *label); + const char *regexp, mode_t type); int clicon_file_copy(char *src, char *target); diff --git a/lib/clixon/clixon_options.h b/lib/clixon/clixon_options.h index 859bfb49..7c776c13 100644 --- a/lib/clixon/clixon_options.h +++ b/lib/clixon/clixon_options.h @@ -92,7 +92,6 @@ char *clicon_cli_dir(clicon_handle h); char *clicon_clispec_dir(clicon_handle h); char *clicon_netconf_dir(clicon_handle h); char *clicon_restconf_dir(clicon_handle h); -char *clicon_archive_dir(clicon_handle h); char *clicon_xmldb_plugin(clicon_handle h); int clicon_sock_family(clicon_handle h); char *clicon_sock(clicon_handle h); diff --git a/lib/src/Makefile.in b/lib/src/Makefile.in index 35221e22..0135d78e 100644 --- a/lib/src/Makefile.in +++ b/lib/src/Makefile.in @@ -61,8 +61,7 @@ CPPFLAGS = @CPPFLAGS@ INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$(top_srcdir) -SRC = clixon_sig.c clixon_log.c clixon_err.c clixon_event.c \ - clixon_chunk.c clixon_proc.c \ +SRC = clixon_sig.c clixon_log.c clixon_err.c clixon_event.c clixon_proc.c \ clixon_string.c clixon_handle.c \ clixon_xml.c clixon_xml_map.c clixon_file.c \ clixon_json.c clixon_yang.c clixon_yang_type.c \ diff --git a/lib/src/clixon_err.c b/lib/src/clixon_err.c index fd47a155..9e900676 100644 --- a/lib/src/clixon_err.c +++ b/lib/src/clixon_err.c @@ -57,7 +57,6 @@ #include "clixon_log.h" #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_err.h" /* @@ -217,7 +216,7 @@ clicon_err_save(void) { struct err_state *es; - if ((es = chunk(sizeof(*es), NULL)) == NULL) + if ((es = malloc(sizeof(*es))) == NULL) return NULL; es->es_errno = clicon_errno; es->es_suberrno = clicon_suberrno; @@ -232,10 +231,11 @@ clicon_err_restore(void* handle) { struct err_state *es; - es = (struct err_state *)handle; - clicon_errno = es->es_errno; - clicon_suberrno = es->es_suberrno; - strncpy(clicon_err_reason, es->es_reason, ERR_STRLEN-1); - unchunk(es); + if ((es = (struct err_state *)handle) != NULL){ + clicon_errno = es->es_errno; + clicon_suberrno = es->es_suberrno; + strncpy(clicon_err_reason, es->es_reason, ERR_STRLEN-1); + free(es); + } return 0; } diff --git a/lib/src/clixon_file.c b/lib/src/clixon_file.c index 64ac8ee1..082ee6ae 100644 --- a/lib/src/clixon_file.c +++ b/lib/src/clixon_file.c @@ -58,7 +58,6 @@ /* clicon */ #include "clixon_err.h" #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_string.h" #include "clixon_file.h" @@ -66,7 +65,8 @@ * qsort function */ static int -clicon_file_dirent_sort(const void* arg1, const void* arg2) +clicon_file_dirent_sort(const void* arg1, + const void* arg2) { struct dirent *d1 = (struct dirent *)arg1; struct dirent *d2 = (struct dirent *)arg2; @@ -81,10 +81,10 @@ clicon_file_dirent_sort(const void* arg1, const void* arg2) /*! Return sorted matching files from a directory * @param[in] dir Directory path - * @param[out] ent Entries pointer, will be filled in with dir entries + * @param[out] ent Entries pointer, will be filled in with dir entries. Free + * after use * @param[in] regexp Regexp filename matching * @param[in] type File type matching, see stat(2) - * @param[in] label Clicon Chunk label for memory handling, unchunk after use * * @retval n Number of matching files in directory * @retval -1 Error @@ -92,34 +92,34 @@ clicon_file_dirent_sort(const void* arg1, const void* arg2) * @code * char *dir = "/root/fs"; * struct dirent *dp; - * if ((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG, __FUNCTION__)) < 0) + * if ((ndp = clicon_file_dirent(dir, &dp, "(.so)$", S_IFREG)) < 0) * return -1; * for (i = 0; i < ndp; i++) * do something with dp[i].d_name; - * unchunk_group(__FUNCTION__); + * free(dp); * @endcode */ int clicon_file_dirent(const char *dir, struct dirent **ent, const char *regexp, - mode_t type, - const char *label) + mode_t type) { - DIR *dirp; - int retval = -1; - int res; - int nent; - char *filename; - regex_t re; - char errbuf[128]; - struct stat st; - struct dirent dent; + int retval = -1; + DIR *dirp; + int res; + int nent; + regex_t re; + char errbuf[128]; + char filename[MAXPATHLEN]; + struct stat st; + struct dirent dent; struct dirent *dresp; struct dirent *tmp; struct dirent *new = NULL; struct dirent *dvecp = NULL; + *ent = NULL; nent = 0; @@ -137,7 +137,9 @@ clicon_file_dirent(const char *dir, goto quit; } - for (res = readdir_r (dirp, &dent, &dresp); dresp; res = readdir_r (dirp, &dent, &dresp)) { + for (res = readdir_r (dirp, &dent, &dresp); + dresp; + res = readdir_r (dirp, &dent, &dresp)) { if (res != 0) { clicon_err(OE_UNIX, 0, "readdir: %s", strerror(errno)); goto quit; @@ -150,12 +152,8 @@ clicon_file_dirent(const char *dir, } /* File type matching */ if (type) { - if ((filename = chunk_sprintf(__FUNCTION__, "%s/%s", dir, dent.d_name)) == NULL) { - clicon_err(OE_UNIX, 0, "chunk: %s", strerror(errno)); - goto quit; - } + snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dent.d_name); res = lstat(filename, &st); - unchunk (filename); if (res != 0) { clicon_err(OE_UNIX, 0, "lstat: %s", strerror(errno)); goto quit; @@ -164,8 +162,8 @@ clicon_file_dirent(const char *dir, continue; } - if ((tmp = rechunk(new, (nent+1)*sizeof(*dvecp), label)) == NULL) { - clicon_err(OE_UNIX, 0, "chunk: %s", strerror(errno)); + if ((tmp = realloc(new, (nent+1)*sizeof(*dvecp))) == NULL) { + clicon_err(OE_UNIX, errno, "realloc"); goto quit; } new = tmp; @@ -183,30 +181,9 @@ quit: closedir(dirp); if (regexp) regfree(&re); - unchunk_group(__FUNCTION__); - return retval; } -/* - * Use mkstep() to create an empty temporary file, accessible only by this user. - * A chunk:ed file name is returned so that caller can overwrite file safely. - */ -char * -clicon_tmpfile(const char *label) -{ - int fd; - char file[] = "/tmp/.tmpXXXXXX"; - - if ((fd = mkstemp(file)) < 0){ - clicon_err(OE_UNIX, errno, "mkstemp"); - return NULL; - } - close(fd); - - return (char *)chunkdup(file, strlen(file)+1, label); -} - /*! Make a copy of file src * @retval 0 OK * @retval -1 Error diff --git a/lib/src/clixon_options.c b/lib/src/clixon_options.c index 11b6d403..1414977a 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -61,7 +61,6 @@ #include "clixon_queue.h" #include "clixon_hash.h" #include "clixon_handle.h" -#include "clixon_chunk.h" #include "clixon_log.h" #include "clixon_yang.h" #include "clixon_options.h" @@ -198,7 +197,6 @@ clicon_option_default(clicon_hash_t *copt) } retval = 0; catch: - unchunk_group(__FUNCTION__); return retval; } @@ -233,10 +231,6 @@ clicon_option_sanity(clicon_hash_t *copt) clicon_err(OE_UNIX, 0, "CLICON_YANG_DIR not defined in config file"); goto done; } - if (!hash_lookup(copt, "CLICON_ARCHIVE_DIR")){ - clicon_err(OE_UNIX, 0, "CLICON_ARCHIVE_DIR not defined in config file"); - goto done; - } if (!hash_lookup(copt, "CLICON_XMLDB_DIR")){ clicon_err(OE_UNIX, 0, "CLICON_XMLDB_DIR not defined in config file"); goto done; @@ -451,12 +445,6 @@ clicon_restconf_dir(clicon_handle h) return clicon_option_str(h, "CLICON_RESTCONF_DIR"); } -char * -clicon_archive_dir(clicon_handle h) -{ - return clicon_option_str(h, "CLICON_ARCHIVE_DIR"); -} - char * clicon_xmldb_plugin(clicon_handle h) { diff --git a/lib/src/clixon_proc.c b/lib/src/clixon_proc.c index 41dd9f8b..cd1e363b 100644 --- a/lib/src/clixon_proc.c +++ b/lib/src/clixon_proc.c @@ -60,7 +60,6 @@ #include "clixon_sig.h" #include "clixon_string.h" #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_proc.h" /* diff --git a/lib/src/clixon_proto.c b/lib/src/clixon_proto.c index 0e14eee2..172aa696 100644 --- a/lib/src/clixon_proto.c +++ b/lib/src/clixon_proto.c @@ -66,7 +66,6 @@ #include "clixon_err.h" #include "clixon_log.h" #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_sig.h" #include "clixon_xml.h" #include "clixon_xsl.h" @@ -532,11 +531,11 @@ send_msg_reply(int s, uint16_t datalen) { int retval = -1; - struct clicon_msg *reply; + struct clicon_msg *reply = NULL; uint16_t len; len = sizeof(*reply) + datalen; - if ((reply = (struct clicon_msg *)chunk(len, __FUNCTION__)) == NULL) + if ((reply = (struct clicon_msg *)malloc(len)) == NULL) goto done; memset(reply, 0, len); reply->op_len = htons(len); @@ -546,7 +545,8 @@ send_msg_reply(int s, goto done; retval = 0; done: - unchunk_group(__FUNCTION__); + if (reply) + free(reply); return retval; } diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c index 65bbb29e..d5a7e977 100644 --- a/lib/src/clixon_proto_client.c +++ b/lib/src/clixon_proto_client.c @@ -56,7 +56,6 @@ /* clicon */ #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_log.h" #include "clixon_hash.h" #include "clixon_handle.h" diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index 66c9c6b4..055c9cc3 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -49,7 +49,6 @@ /* clicon */ #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_string.h" #include "clixon_err.h" diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index f8134c0e..bfcbcd61 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -51,7 +51,6 @@ #include "clixon_err.h" #include "clixon_log.h" #include "clixon_queue.h" -#include "clixon_chunk.h" #include "clixon_xml.h" #include "clixon_xml_parse.h" diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 66350e48..62d724f3 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -46,7 +46,6 @@ #include #include #include -#include #include /* cligen */ @@ -128,14 +127,36 @@ xmldb_get(clicon_handle h, char *db, char *xpath, clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_get_fn && - _xa_api->xa_get_fn(h, db, xpath, xtop, xvec, xlen) < 0) + if (_xa_api->xa_get_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_get_fn(h, db, xpath, xtop, xvec, xlen); done: return retval; } +/*! Modify database provided an xml tree and an operation + * + * @param[in] h CLICON handle + * @param[in] db running or candidate + * @param[in] xt xml-tree. Top-level symbol is dummy + * @param[in] op OP_MERGE: just add it. + * OP_REPLACE: first delete whole database + * OP_NONE: operation attribute in xml determines operation + * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) + * @retval 0 OK + * @retval -1 Error + * The xml may contain the "operation" attribute which defines the operation. + * @code + * cxobj *xt; + * if (clicon_xml_parse_str("17", &xt) < 0) + * err; + * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) + * err; + * @endcode + * @see xmldb_put_xkey for single key + */ int xmldb_put(clicon_handle h, char *db, enum operation_type op, char *api_path, cxobj *xt) @@ -146,16 +167,24 @@ xmldb_put(clicon_handle h, char *db, enum operation_type op, clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_put_fn && - _xa_api->xa_put_fn(h, db, op, api_path, xt) < 0) + if (_xa_api->xa_put_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_put_fn(h, db, op, api_path, xt); done: return retval; } +/*! Raw dump of database, in internal format (depends on datastore) + * @param[in] f File + * @param[in] dbfile File-name of database. This is a local file + * @param[in] pattern Key regexp, eg "^.*$" + */ int -xmldb_dump(FILE *f, char *dbfilename, char *rxkey) +xmldb_dump(FILE *f, + char *dbfilename, + char *pattern) { int retval = -1; @@ -163,14 +192,22 @@ xmldb_dump(FILE *f, char *dbfilename, char *rxkey) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_dump_fn && - _xa_api->xa_dump_fn(f, dbfilename, rxkey) < 0) + if (_xa_api->xa_dump_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_dump_fn(f, dbfilename, pattern); done: return retval; } +/*! Copy database from db1 to db2 + * @param[in] h Clicon handle + * @param[in] from Source database copy + * @param[in] to Destination database + * @retval -1 Error + * @retval 0 OK + */ int xmldb_copy(clicon_handle h, char *from, char *to) { @@ -180,14 +217,22 @@ xmldb_copy(clicon_handle h, char *from, char *to) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_copy_fn && - _xa_api->xa_copy_fn(h, from, to) < 0) + if (_xa_api->xa_copy_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_copy_fn(h, from, to); done: return retval; } +/*! Lock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + */ int xmldb_lock(clicon_handle h, char *db, int pid) { @@ -197,14 +242,23 @@ xmldb_lock(clicon_handle h, char *db, int pid) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_lock_fn && - _xa_api->xa_lock_fn(h, db, pid) < 0) + if (_xa_api->xa_lock_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_lock_fn(h, db, pid); done: return retval; } +/*! Unlock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + * Assume all sanity checks have been made + */ int xmldb_unlock(clicon_handle h, char *db, int pid) { @@ -214,14 +268,21 @@ xmldb_unlock(clicon_handle h, char *db, int pid) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_unlock_fn && - _xa_api->xa_unlock_fn(h, db, pid) < 0) + if (_xa_api->xa_unlock_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_unlock_fn(h, db, pid); done: return retval; } +/*! Unlock all databases locked by pid (eg process dies) + * @param[in] h Clicon handle + * @param[in] pid Process / Session id + * @retval -1 Error + * @retval 0 OK + */ int xmldb_unlock_all(clicon_handle h, int pid) { @@ -231,14 +292,22 @@ xmldb_unlock_all(clicon_handle h, int pid) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_unlock_all_fn && - _xa_api->xa_unlock_all_fn(h, pid) < 0) + if (_xa_api->xa_unlock_all_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval =_xa_api->xa_unlock_all_fn(h, pid); done: return retval; } +/*! Check if database is locked + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 Not locked + * @retval >0 Id of locker + */ int xmldb_islocked(clicon_handle h, char *db) { @@ -248,14 +317,22 @@ xmldb_islocked(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_islocked_fn && - _xa_api->xa_islocked_fn(h, db) < 0) + if (_xa_api->xa_islocked_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval =_xa_api->xa_islocked_fn(h, db); done: return retval; } +/*! Check if db exists + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 No it does not exist + * @retval 1 Yes it exists + */ int xmldb_exists(clicon_handle h, char *db) { @@ -265,14 +342,21 @@ xmldb_exists(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_exists_fn && - _xa_api->xa_exists_fn(h, db) < 0) + if (_xa_api->xa_exists_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_exists_fn(h, db); done: return retval; } +/*! Delete database. Remove file + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 OK + */ int xmldb_delete(clicon_handle h, char *db) { @@ -282,14 +366,21 @@ xmldb_delete(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_delete_fn && - _xa_api->xa_delete_fn(h, db) < 0) + if (_xa_api->xa_delete_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_delete_fn(h, db); done: return retval; } +/*! Initialize database + * @param[in] h Clicon handle + * @param[in] db Database + * @retval 0 OK + * @retval -1 Error + */ int xmldb_init(clicon_handle h, char *db) { @@ -299,10 +390,11 @@ xmldb_init(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_init_fn && - _xa_api->xa_init_fn(h, db) < 0) + if (_xa_api->xa_init_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); goto done; - retval = 0; + } + retval = _xa_api->xa_init_fn(h, db); done: return retval; } diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 563cba7e..dbdf5c8f 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -76,7 +76,6 @@ #include "clixon_string.h" #include "clixon_queue.h" #include "clixon_hash.h" -#include "clixon_chunk.h" #include "clixon_handle.h" #include "clixon_yang.h" #include "clixon_yang_type.h" @@ -125,8 +124,9 @@ xml2txt(FILE *f, cxobj *x, int level) { cxobj *xe = NULL; int children=0; - char *term; + char *term = NULL; int retval = -1; + int encr=0; #ifdef SPECIAL_TREATMENT_OF_NAME cxobj *xname; #endif @@ -138,15 +138,17 @@ xml2txt(FILE *f, cxobj *x, int level) if (xml_type(x) == CX_BODY){ /* Kludge for escaping encrypted passwords */ if (strcmp(xml_name(xml_parent(x)), "encrypted-password")==0) - term = chunk_sprintf(__FUNCTION__, "\"%s\"", xml_value(x)); - else - term = xml_value(x); + encr++; + term = xml_value(x); } else{ fprintf(f, "%*s", 4*level, ""); term = xml_name(x); } - fprintf(f, "%s;\n", term); + if (encr) + fprintf(f, "\"%s\";\n", term); + else + fprintf(f, "%s;\n", term); retval = 0; goto done; } diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 7e0d0bf4..af1f33ea 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -66,7 +66,6 @@ #include "clixon_file.h" #include "clixon_yang.h" #include "clixon_hash.h" -#include "clixon_chunk.h" #include "clixon_options.h" #include "clixon_yang_type.h" #include "clixon_yang_parse.h" @@ -1382,20 +1381,21 @@ yang_parse_file(clicon_handle h, * @retval 1 Match founbd, Most recent entry returned in fbuf * @retval 0 No matching entry found * @retval -1 Error -*/static int +*/ +static int yang_parse_find_match(clicon_handle h, const char *yang_dir, const char *module, cbuf *fbuf) { int retval = -1; - struct dirent *dp; + struct dirent *dp = NULL; int ndp; cbuf *regex = NULL; char *regexstr; if ((regex = cbuf_new()) == NULL){ - clicon_err(OE_YANG, errno, "%s: cbuf_new", __FUNCTION__); + clicon_err(OE_YANG, errno, "cbuf_new"); goto done; } cprintf(regex, "^%s.*(.yang)$", module); @@ -1403,8 +1403,7 @@ yang_parse_find_match(clicon_handle h, if ((ndp = clicon_file_dirent(yang_dir, &dp, regexstr, - S_IFREG, - __FUNCTION__)) < 0) + S_IFREG)) < 0) goto done; /* Entries are sorted, last entry should be most recent date */ if (ndp != 0){ @@ -1416,7 +1415,8 @@ yang_parse_find_match(clicon_handle h, done: if (regex) cbuf_free(regex); - unchunk_group(__FUNCTION__); + if (dp) + free(dp); return retval; } diff --git a/lib/src/clixon_yang_type.c b/lib/src/clixon_yang_type.c index 6acb1e36..dbb5ac65 100644 --- a/lib/src/clixon_yang_type.c +++ b/lib/src/clixon_yang_type.c @@ -63,7 +63,6 @@ #include "clixon_handle.h" #include "clixon_yang.h" #include "clixon_hash.h" -#include "clixon_chunk.h" #include "clixon_options.h" #include "clixon_yang.h" #include "clixon_yang_type.h" From af334bb7469d9a4949b5c47eb9b6ea7845ce9813 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 10 Apr 2017 21:52:39 +0200 Subject: [PATCH 03/24] Removed curl dependency --- CHANGELOG | 6 +- apps/restconf/restconf_lib.c | 7 +- configure | 49 -------------- configure.ac | 3 - datastore/keyvalue/clixon_keyvalue.c | 34 ++++------ include/clixon_config.h.in | 3 - lib/clixon/clixon_string.h | 2 + lib/src/clixon_string.c | 96 ++++++++++++++++++++++++++++ lib/src/clixon_xml_map.c | 10 ++- 9 files changed, 121 insertions(+), 89 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 48032069..e61c2400 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -29,8 +29,10 @@ # # ***** END LICENSE BLOCK ***** -- Moved qdbm, chunk and xmldb to datastore keyvalue directories - Created xmldb plugin api +- Removed curl dependency + +- Created xmldb plugin api + Moved qdbm, chunk and xmldb to datastore keyvalue directories Removed all other clixon dependency on chunk code - cli_copy_config added as generic cli command diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 9515f4ff..cd933a36 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -48,7 +48,6 @@ #include #include #include -#include /* cligen */ #include @@ -163,10 +162,8 @@ str2cvec(char *string, *(snext++) = '\0'; if ((val = index(s, delim2)) != NULL){ *(val++) = '\0'; - if ((valu = curl_easy_unescape(NULL, val, 0, NULL)) == NULL){ - clicon_debug(1, "curl_easy_unescape %s", strerror(errno)); + if (percent_decode(val, &valu) < 0) goto err; - } if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ clicon_debug(1, "error cvec_add %s", strerror(errno)); goto err; @@ -175,7 +172,7 @@ str2cvec(char *string, s++; cv_name_set(cv, s); cv_string_set(cv, valu); - curl_free(valu); + free(valu); valu = NULL; } else{ if (strlen(s)){ diff --git a/configure b/configure index bf0f6ace..6140aa7d 100755 --- a/configure +++ b/configure @@ -4172,55 +4172,6 @@ _ACEOF fi -# restconf uses libcurl (I think?) -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for curl_global_init in -lcurl" >&5 -$as_echo_n "checking for curl_global_init in -lcurl... " >&6; } -if ${ac_cv_lib_curl_curl_global_init+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS -LIBS="-lcurl $LIBS" -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -/* Override any GCC internal prototype to avoid an error. - Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -#ifdef __cplusplus -extern "C" -#endif -char curl_global_init (); -int -main () -{ -return curl_global_init (); - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_curl_curl_global_init=yes -else - ac_cv_lib_curl_curl_global_init=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_curl_curl_global_init" >&5 -$as_echo "$ac_cv_lib_curl_curl_global_init" >&6; } -if test "x$ac_cv_lib_curl_curl_global_init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCURL 1 -_ACEOF - - LIBS="-lcurl $LIBS" - -else - as_fn_error $? "libcurl missing" "$LINENO" 5 -fi - - for ac_func in inet_aton sigaction sigvec strlcpy strsep strndup alphasort versionsort strverscmp do : as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` diff --git a/configure.ac b/configure.ac index d8ff3cda..c97cf5a3 100644 --- a/configure.ac +++ b/configure.ac @@ -151,9 +151,6 @@ AC_CHECK_LIB(socket, socket) AC_CHECK_LIB(nsl, xdr_char) AC_CHECK_LIB(dl, dlopen) -# restconf uses libcurl (I think?) -AC_CHECK_LIB(curl, curl_global_init,, AC_MSG_ERROR([libcurl missing])) - AC_CHECK_FUNCS(inet_aton sigaction sigvec strlcpy strsep strndup alphasort versionsort strverscmp) # Lives in libfcgi-dev diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 84c31f94..fcbab626 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -100,7 +100,6 @@ #include #include #include -#include /* cligen */ #include @@ -108,6 +107,7 @@ /* clicon */ #include +#include "clixon_chunk.h" #include "clixon_qdb.h" #include "clixon_keyvalue.h" @@ -200,16 +200,15 @@ append_listkeys(cbuf *ckey, xml_name(xt), keyname); goto done; } - if ((bodyenc = curl_easy_escape(NULL, xml_body(xkey), 0)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_encode(xml_body(xkey), &bodyenc) < 0) goto done; - } if (i++) cprintf(ckey, ","); else cprintf(ckey, "="); cprintf(ckey, "%s", bodyenc); - curl_free(bodyenc); bodyenc = NULL; + free(bodyenc); + bodyenc = NULL; } retval = 0; done: @@ -362,17 +361,15 @@ get(char *dbname, * If xml element is a leaf-list, then the next element is expected to * be a value */ - if ((argdec = curl_easy_unescape(NULL, restval, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_decode(restval, &argdec) < 0) goto done; - } if ((xc = xml_find(x, name))==NULL || (xb = xml_find(xc, argdec))==NULL){ if ((xc = xml_new_spec(name, x, y)) == NULL) goto done; /* Assume body is created at end of function */ } - curl_free(argdec); + free(argdec); argdec = NULL; break; case Y_LIST: @@ -409,12 +406,10 @@ get(char *dbname, if (j>=nvalvec) break; arg = valvec[j++]; - if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_decode(arg, &argdec) < 0) goto done; - } cprintf(cb, "[%s=%s]", cv_string_get(cvi), argdec); - curl_free(argdec); + free(argdec); argdec=NULL; } if ((xc = xpath_first(x, cbuf_get(cb))) == NULL){ @@ -429,16 +424,15 @@ get(char *dbname, break; arg = valvec[j++]; keyname = cv_string_get(cvi); - if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_decode(arg, &argdec) < 0) goto done; - } if (create_keyvalues(xc, ykey, argdec, keyname) < 0) goto done; - curl_free(argdec); argdec = NULL; + free(argdec); + argdec = NULL; } /* while */ } if (cb){ @@ -768,10 +762,8 @@ put(char *dbname, goto done; break; case Y_LEAF_LIST: - if ((bodyenc = curl_easy_escape(NULL, body, 0)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_encode(body, &bodyenc) < 0) goto done; - } cprintf(cbxk, "=%s", bodyenc); break; default: @@ -821,7 +813,7 @@ put(char *dbname, if (cbxk) cbuf_free(cbxk); if (bodyenc) - curl_free(bodyenc); + free(bodyenc); return retval; } diff --git a/include/clixon_config.h.in b/include/clixon_config.h.in index 736baf37..1b450323 100644 --- a/include/clixon_config.h.in +++ b/include/clixon_config.h.in @@ -33,9 +33,6 @@ /* Define to 1 if you have the `crypt' library (-lcrypt). */ #undef HAVE_LIBCRYPT -/* Define to 1 if you have the `curl' library (-lcurl). */ -#undef HAVE_LIBCURL - /* Define to 1 if you have the `dl' library (-ldl). */ #undef HAVE_LIBDL diff --git a/lib/clixon/clixon_string.h b/lib/clixon/clixon_string.h index 86c22e6d..d7fd4274 100644 --- a/lib/clixon/clixon_string.h +++ b/lib/clixon/clixon_string.h @@ -59,5 +59,7 @@ char *clicon_strjoin (int argc, char **argv, char *delim); #ifndef HAVE_STRNDUP char *clicon_strndup (const char *, size_t); #endif /* ! HAVE_STRNDUP */ +int percent_encode(char *str, char **escp); +int percent_decode(char *esc, char **str); #endif /* _CLIXON_STRING_H_ */ diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index 055c9cc3..aecb7977 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -137,6 +137,102 @@ clicon_strjoin(int argc, return str; } +static int +unreserved(unsigned char in) +{ + switch(in) { + case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + case 'a': case 'b': case 'c': case 'd': case 'e': + case 'f': case 'g': case 'h': case 'i': case 'j': + case 'k': case 'l': case 'm': case 'n': case 'o': + case 'p': case 'q': case 'r': case 's': case 't': + case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': + case 'A': case 'B': case 'C': case 'D': case 'E': + case 'F': case 'G': case 'H': case 'I': case 'J': + case 'K': case 'L': case 'M': case 'N': case 'O': + case 'P': case 'Q': case 'R': case 'S': case 'T': + case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': + case '-': case '.': case '_': case '~': + return 1; + default: + break; + } + return 0; +} + +/*! Percent encoding according to RFC 3896 + * @param[out] esc Deallocate with free() + */ +int +percent_encode(char *str, + char **escp) +{ + int retval = -1; + char *esc = NULL; + int i, j; + + /* This is max */ + if ((esc = malloc(strlen(str)*3+1)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + j = 0; + for (i=0; i 2 && + isxdigit(esc[i+1]) && isxdigit(esc[i+2])){ + hstr[0] = esc[i+1]; + hstr[1] = esc[i+2]; + hstr[2] = 0; + str[j] = strtoul(hstr, &ptr, 16); + i += 2; + } + else + str[j] = esc[i]; + j++; + } + *strp = str; + retval = 0; + done: + if (retval < 0 && str) + free(str); + return retval; +} /*! strndup() for systems without it, such as xBSD */ diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index dbdf5c8f..c08deb3a 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -66,7 +66,6 @@ #include #include #include -#include /* cligen */ #include @@ -77,6 +76,7 @@ #include "clixon_queue.h" #include "clixon_hash.h" #include "clixon_handle.h" +#include "clixon_string.h" #include "clixon_yang.h" #include "clixon_yang_type.h" #include "clixon_options.h" @@ -957,13 +957,11 @@ xmlkeyfmt2key(char *xkfmt, clicon_err(OE_UNIX, errno, "strdup"); goto done; } - if ((strenc = curl_easy_escape(NULL, str, 0)) == NULL){ - clicon_err(OE_UNIX, errno, "curl_easy_escape"); + if (percent_encode(str, &strenc) < 0) goto done; - } cprintf(cb, "%s", strenc); - curl_free(strenc); - free(str); + free(strenc); strenc = NULL; + free(str); str = NULL; } else if (c == '%') From 2758a30bb7e0deccc1c3bcd09ad847989ecb10d0 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 11 Apr 2017 20:15:43 +0200 Subject: [PATCH 04/24] Configure (autoconf) changes: Removed libcurl dependency, Disable restconf and disable restconf options. Added empty text datastore --- CHANGELOG => CHANGELOG.txt | 7 +- apps/Makefile.in | 9 +- apps/backend/backend_main.c | 4 +- configure | 153 ++++---- configure.ac | 43 ++- datastore/Makefile.in | 7 +- datastore/keyvalue/Makefile.in | 5 +- datastore/keyvalue/clixon_chunk.c | 7 +- datastore/keyvalue/clixon_keyvalue.c | 4 +- datastore/keyvalue/clixon_qdb.c | 5 +- datastore/text/Makefile.in | 97 ++++++ datastore/text/clixon_xmldb_text.c | 501 +++++++++++++++++++++++++++ datastore/text/clixon_xmldb_text.h | 56 +++ lib/clixon/clixon.h | 91 ----- lib/clixon/clixon.h.in | 5 +- 15 files changed, 812 insertions(+), 182 deletions(-) rename CHANGELOG => CHANGELOG.txt (95%) create mode 100644 datastore/text/Makefile.in create mode 100644 datastore/text/clixon_xmldb_text.c create mode 100644 datastore/text/clixon_xmldb_text.h delete mode 100644 lib/clixon/clixon.h diff --git a/CHANGELOG b/CHANGELOG.txt similarity index 95% rename from CHANGELOG rename to CHANGELOG.txt index e61c2400..6b28a139 100644 --- a/CHANGELOG +++ b/CHANGELOG.txt @@ -29,7 +29,12 @@ # # ***** END LICENSE BLOCK ***** -- Removed curl dependency +- Added datastore 'text' + +- Configure (autoconf) changes + Removed libcurl dependency + Disable restconf (and fastcgi) with configure --disable-restconf + Disable keyvalue datastore (and qdbm) with configure --disable-keyvalue - Created xmldb plugin api Moved qdbm, chunk and xmldb to datastore keyvalue directories diff --git a/apps/Makefile.in b/apps/Makefile.in index a685dcaa..7f7ca9c0 100644 --- a/apps/Makefile.in +++ b/apps/Makefile.in @@ -37,10 +37,17 @@ CC = @CC@ CFLAGS = @CFLAGS@ LDFLAGS = @LDFLAGS@ LIBS = @LIBS@ +with_restconf = @with_restconf@ SHELL = /bin/sh -SUBDIRS = cli backend dbctrl netconf restconf +SUBDIRS = backend +SUBDIRS += cli +SUBDIRS += dbctrl +SUBDIRS += netconf +ifeq ($(with_restconf),yes) +SUBDIRS += restconf +endif .PHONY: all clean depend install $(SUBDIRS) diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 1b832fe9..f417e2ec 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -141,8 +141,8 @@ usage(char *argv0, clicon_handle h) " -p \t\tPrint database yang specification\n" " -t \t\tPrint alternate spec translation (eg if YANG print KEY, if KEY print YANG)\n" " -g \tClient membership required to this group (default: %s)\n" - "\t-y \tOverride yang spec file (dont include .yang suffix)\n" - "\t-x \tXMLDB plugin\n", + " -y \tOverride yang spec file (dont include .yang suffix)\n" + " -x \tXMLDB plugin\n", argv0, plgdir ? plgdir : "none", confsock ? confsock : "none", diff --git a/configure b/configure index 6140aa7d..f0d9de95 100755 --- a/configure +++ b/configure @@ -632,6 +632,8 @@ CPP OBJEXT EXEEXT ac_ct_CC +with_keyvalue +with_restconf RANLIB AR EXE_SUFFIX @@ -702,6 +704,8 @@ ac_subst_files='' ac_user_opts=' enable_option_checking with_cligen +with_restconf +with_keyvalue with_qdbm ' ac_precious_vars='build_alias @@ -1327,6 +1331,8 @@ Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] --without-PACKAGE do not use PACKAGE (same as --with-PACKAGE=no) --with-cligen=dir Use CLIGEN here + --without-restconf disable support for restconf + --without-keyvalue disable support for key-value xmldb datastore --with-qdbm=dir Use QDBM here Some influential environment variables: @@ -2320,6 +2326,8 @@ test -n "$target_alias" && + # If yes, compile apps/restconf + # If yes, compile datastore/keyvalue # ac_ext=c @@ -3534,7 +3542,6 @@ if test "${with_cligen}"; then fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for grep that handles long lines and -e" >&5 $as_echo_n "checking for grep that handles long lines and -e... " >&6; } if ${ac_cv_path_GREP+:} false; then : @@ -3859,21 +3866,92 @@ else fi -# This is for qdbm +# This is for restconf (and fastcgi) + +# Check whether --with-restconf was given. +if test "${with_restconf+set}" = set; then : + withval=$with_restconf; +else + with_restconf=yes +fi + +if test "x${with_restconf}" == xyes; then + # Lives in libfcgi-dev + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for FCGX_Init in -lfcgi" >&5 +$as_echo_n "checking for FCGX_Init in -lfcgi... " >&6; } +if ${ac_cv_lib_fcgi_FCGX_Init+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lfcgi $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +/* Override any GCC internal prototype to avoid an error. + Use char because int might match the return type of a GCC + builtin and then its argument prototype would still apply. */ +#ifdef __cplusplus +extern "C" +#endif +char FCGX_Init (); +int +main () +{ +return FCGX_Init (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_fcgi_FCGX_Init=yes +else + ac_cv_lib_fcgi_FCGX_Init=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_fcgi_FCGX_Init" >&5 +$as_echo "$ac_cv_lib_fcgi_FCGX_Init" >&6; } +if test "x$ac_cv_lib_fcgi_FCGX_Init" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBFCGI 1 +_ACEOF + + LIBS="-lfcgi $LIBS" + +else + as_fn_error $? "libfcgi-dev missing" "$LINENO" 5 +fi + +fi + +# This is for keyvalue datastore (and qdbm) + +# Check whether --with-keyvalue was given. +if test "${with_keyvalue+set}" = set; then : + withval=$with_keyvalue; +else + with_keyvalue=yes +fi + + +if test "x${with_keyvalue}" == xyes; then + echo "yes keyvalue" + # This is for qdbm # Check whether --with-qdbm was given. if test "${with_qdbm+set}" = set; then : withval=$with_qdbm; fi -if test "${with_qdbm}"; then - echo "Using QDBM here: ${with_qdbm}" - CPPFLAGS="-I${with_qdbm}/include ${CPPFLAGS}" - LDFLAGS="-L${with_qdbm}/lib ${LDFLAGS}" -fi - -# Problem: depot.h may be in qdbm/depot.h. -for ac_header in depot.h + if test "${with_qdbm}"; then + echo "Using QDBM here: ${with_qdbm}" + CPPFLAGS="-I${with_qdbm}/include ${CPPFLAGS}" + LDFLAGS="-L${with_qdbm}/lib ${LDFLAGS}" + fi + # Problem: depot.h may be in qdbm/depot.h. + for ac_header in depot.h do : ac_fn_c_check_header_mongrel "$LINENO" "depot.h" "ac_cv_header_depot_h" "$ac_includes_default" if test "x$ac_cv_header_depot_h" = xyes; then : @@ -3900,7 +3978,7 @@ fi done -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for dpopen in -lqdbm" >&5 + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dpopen in -lqdbm" >&5 $as_echo_n "checking for dpopen in -lqdbm... " >&6; } if ${ac_cv_lib_qdbm_dpopen+:} false; then : $as_echo_n "(cached) " >&6 @@ -3947,6 +4025,7 @@ else as_fn_error $? "libqdbm-dev required" "$LINENO" 5 fi +fi { $as_echo "$as_me:${as_lineno-$LINENO}: checking for crypt in -lcrypt" >&5 $as_echo_n "checking for crypt in -lcrypt... " >&6; } @@ -4185,58 +4264,9 @@ fi done -# Lives in libfcgi-dev -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for FCGX_Init in -lfcgi" >&5 -$as_echo_n "checking for FCGX_Init in -lfcgi... " >&6; } -if ${ac_cv_lib_fcgi_FCGX_Init+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS -LIBS="-lfcgi $LIBS" -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -/* Override any GCC internal prototype to avoid an error. - Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -#ifdef __cplusplus -extern "C" -#endif -char FCGX_Init (); -int -main () -{ -return FCGX_Init (); - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_fcgi_FCGX_Init=yes -else - ac_cv_lib_fcgi_FCGX_Init=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_fcgi_FCGX_Init" >&5 -$as_echo "$ac_cv_lib_fcgi_FCGX_Init" >&6; } -if test "x$ac_cv_lib_fcgi_FCGX_Init" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBFCGI 1 -_ACEOF - - LIBS="-lfcgi $LIBS" - -else - as_fn_error $? "libfcgi-dev missing" "$LINENO" 5 -fi - - -ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile doc/Makefile" +ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile datastore/text/Makefile doc/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -4953,6 +4983,7 @@ do "docker/netconf/Dockerfile") CONFIG_FILES="$CONFIG_FILES docker/netconf/Dockerfile" ;; "datastore/Makefile") CONFIG_FILES="$CONFIG_FILES datastore/Makefile" ;; "datastore/keyvalue/Makefile") CONFIG_FILES="$CONFIG_FILES datastore/keyvalue/Makefile" ;; + "datastore/text/Makefile") CONFIG_FILES="$CONFIG_FILES datastore/text/Makefile" ;; "doc/Makefile") CONFIG_FILES="$CONFIG_FILES doc/Makefile" ;; *) as_fn_error $? "invalid argument: \`$ac_config_target'" "$LINENO" 5;; diff --git a/configure.ac b/configure.ac index c97cf5a3..9b12e28d 100644 --- a/configure.ac +++ b/configure.ac @@ -77,6 +77,8 @@ AC_SUBST(SH_SUFFIX) AC_SUBST(EXE_SUFFIX) AC_SUBST(AR) AC_SUBST(RANLIB) +AC_SUBST(with_restconf) # If yes, compile apps/restconf +AC_SUBST(with_keyvalue) # If yes, compile datastore/keyvalue # AC_PROG_CC() @@ -119,22 +121,39 @@ if test "${with_cligen}"; then LDFLAGS="-L${with_cligen}/lib ${LDFLAGS}" fi - AC_CHECK_HEADERS(cligen/cligen.h,, AC_MSG_ERROR(cligen missing. Try: git clone https://github.com/olofhagsand/cligen.git)) AC_CHECK_LIB(:libcligen.so.${CLIGEN_VERSION}, cligen_init,, AC_MSG_ERROR([CLIgen${CLIGEN_VERSION} missing. Try: git clone https://github.com/olofhagsand/cligen.git])) -# This is for qdbm -AC_ARG_WITH(qdbm, [ --with-qdbm=dir Use QDBM here ] ) -if test "${with_qdbm}"; then - echo "Using QDBM here: ${with_qdbm}" - CPPFLAGS="-I${with_qdbm}/include ${CPPFLAGS}" - LDFLAGS="-L${with_qdbm}/lib ${LDFLAGS}" +# This is for restconf (and fastcgi) +AC_ARG_WITH([restconf], + [AS_HELP_STRING([--without-restconf],[disable support for restconf])], + [], + [with_restconf=yes]) +if test "x${with_restconf}" == xyes; then + # Lives in libfcgi-dev + AC_CHECK_LIB(fcgi, FCGX_Init,, AC_MSG_ERROR([libfcgi-dev missing])) fi -# Problem: depot.h may be in qdbm/depot.h. -AC_CHECK_HEADERS(depot.h,,[AC_CHECK_HEADERS(qdbm/depot.h,,AC_MSG_ERROR(libqdbm-dev required))]) -AC_CHECK_LIB(qdbm, dpopen,, AC_MSG_ERROR(libqdbm-dev required)) +# This is for keyvalue datastore (and qdbm) +AC_ARG_WITH([keyvalue], + [AS_HELP_STRING([--without-keyvalue],[disable support for key-value xmldb datastore])], + [], + [with_keyvalue=yes]) + +if test "x${with_keyvalue}" == xyes; then + echo "yes keyvalue" + # This is for qdbm + AC_ARG_WITH(qdbm, [ --with-qdbm=dir Use QDBM here, if keyvalue ] ) + if test "${with_qdbm}"; then + echo "Using QDBM here: ${with_qdbm}" + CPPFLAGS="-I${with_qdbm}/include ${CPPFLAGS}" + LDFLAGS="-L${with_qdbm}/lib ${LDFLAGS}" + fi + # Problem: depot.h may be in qdbm/depot.h. + AC_CHECK_HEADERS(depot.h,,[AC_CHECK_HEADERS(qdbm/depot.h,,AC_MSG_ERROR(libqdbm-dev required))]) + AC_CHECK_LIB(qdbm, dpopen,, AC_MSG_ERROR(libqdbm-dev required)) +fi AC_CHECK_LIB(crypt, crypt) AC_CHECK_HEADERS(crypt.h) @@ -153,9 +172,6 @@ AC_CHECK_LIB(dl, dlopen) AC_CHECK_FUNCS(inet_aton sigaction sigvec strlcpy strsep strndup alphasort versionsort strverscmp) -# Lives in libfcgi-dev -AC_CHECK_LIB(fcgi, FCGX_Init,, AC_MSG_ERROR([libfcgi-dev missing])) - AH_BOTTOM([#include ]) AC_OUTPUT(Makefile @@ -182,6 +198,7 @@ AC_OUTPUT(Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile + datastore/text/Makefile doc/Makefile ) diff --git a/datastore/Makefile.in b/datastore/Makefile.in index 7ad7112f..76e7a461 100644 --- a/datastore/Makefile.in +++ b/datastore/Makefile.in @@ -37,10 +37,15 @@ CC = @CC@ CFLAGS = @CFLAGS@ LDFLAGS = @LDFLAGS@ LIBS = @LIBS@ +with_restconf = @with_restconf@ +with_keyvalue = @with_keyvalue@ SHELL = /bin/sh -SUBDIRS = keyvalue +SUBDIRS = text +ifeq ($(with_keyvalue),yes) +SUBDIRS += keyvalue +endif .PHONY: all clean depend install $(SUBDIRS) diff --git a/datastore/keyvalue/Makefile.in b/datastore/keyvalue/Makefile.in index 0737450e..7db2ef56 100644 --- a/datastore/keyvalue/Makefile.in +++ b/datastore/keyvalue/Makefile.in @@ -51,7 +51,7 @@ LIBS = @LIBS@ DATASTORE = keyvalue CPPFLAGS = @CPPFLAGS@ -INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$(top_srcdir) +INCLUDES = -I. -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ PLUGIN = $(DATASTORE).so @@ -63,9 +63,6 @@ all: $(PLUGIN) -include $(DESTDIR)$(datarootdir)/clixon/clixon.mk -#grideye_sysinfo.so.1: grideye_sysinfo.c -# $(CC) $(CFLAGS) -shared -o $@ -lc $^ -lm - $(PLUGIN): $(SRC) $(CC) $(CPPFLAGS) $(INCLUDES) $(CFLAGS) -shared -o $@ -lc $^ $(LIBS) diff --git a/datastore/keyvalue/clixon_chunk.c b/datastore/keyvalue/clixon_chunk.c index 4d4782a0..bade74fa 100644 --- a/datastore/keyvalue/clixon_chunk.c +++ b/datastore/keyvalue/clixon_chunk.c @@ -36,6 +36,7 @@ ensure errno is set and return -1/NULL */ #include #include +#include #include #include #include @@ -45,7 +46,11 @@ #include /* clicon */ -#include "clixon_queue.h" +#include + +/* clicon */ +#include + #include "clixon_chunk.h" /* diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index fcbab626..68e67a51 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -1552,7 +1552,7 @@ kv_init(clicon_handle h, /*! plugin init function */ int -keyvalue_plugin_exit(void) +kv_plugin_exit(void) { return 0; } @@ -1577,7 +1577,7 @@ static const struct xmldb_api api = { 1, XMLDB_API_MAGIC, clixon_xmldb_plugin_init, - keyvalue_plugin_exit, + kv_plugin_exit, kv_get, kv_put, kv_dump, diff --git a/datastore/keyvalue/clixon_qdb.c b/datastore/keyvalue/clixon_qdb.c index 9b716951..1af75ab0 100644 --- a/datastore/keyvalue/clixon_qdb.c +++ b/datastore/keyvalue/clixon_qdb.c @@ -78,9 +78,8 @@ #include /* clicon */ -#include "clixon_log.h" -#include "clixon_err.h" -#include "clixon_queue.h" +#include + #include "clixon_chunk.h" #include "clixon_qdb.h" diff --git a/datastore/text/Makefile.in b/datastore/text/Makefile.in new file mode 100644 index 00000000..7b7cf6e9 --- /dev/null +++ b/datastore/text/Makefile.in @@ -0,0 +1,97 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# +# Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren +# +# This file is part of CLIXON +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Alternatively, the contents of this file may be used under the terms of +# the GNU General Public License Version 3 or later (the "GPL"), +# in which case the provisions of the GPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of the GPL, and not to allow others to +# use your version of this file under the terms of Apache License version 2, +# indicate your decision by deleting the provisions above and replace them with +# the notice and other provisions required by the GPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the Apache License version 2 or the GPL. +# +# ***** END LICENSE BLOCK ***** +# +prefix = @prefix@ +datarootdir = @datarootdir@ +srcdir = @srcdir@ +top_srcdir = @top_srcdir@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +libdir = @libdir@ +dbdir = @prefix@/db +mandir = @mandir@ +libexecdir = @libexecdir@ +localstatedir = @localstatedir@ +sysconfdir = @sysconfdir@ + +VPATH = @srcdir@ +CC = @CC@ +CFLAGS = @CFLAGS@ -rdynamic -fPIC +LDFLAGS = @LDFLAGS@ +LIBS = @LIBS@ +DATASTORE = text +CPPFLAGS = @CPPFLAGS@ + +INCLUDES = -I. -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ + +PLUGIN = $(DATASTORE).so + +SRC = clixon_xmldb_text.c + +OBJS = $(SRC:.c=.o) + +all: $(PLUGIN) + +-include $(DESTDIR)$(datarootdir)/clixon/clixon.mk + +$(PLUGIN): $(SRC) + $(CC) $(CPPFLAGS) $(INCLUDES) $(CFLAGS) -shared -o $@ -lc $^ $(LIBS) + +clean: + rm -f $(PLUGIN) $(OBJS) *.core + +distclean: clean + rm -f Makefile *~ .depend + +.SUFFIXES: +.SUFFIXES: .c .o + +.c.o: $(SRC) + $(CC) $(INCLUDES) $(CPPFLAGS) $(CFLAGS) -c $< + +install: $(PLUGIN) + install -d $(DESTDIR)$(clixon_LIBDIR)/xmldb + install $(PLUGIN) $(DESTDIR)$(clixon_LIBDIR)/xmldb; + +install-include: + +uninstall: + rm -rf $(DESTDIR)$(clixon_LIBDIR)/xmldb/$(PLUGIN); + +TAGS: + find . -name '*.[chyl]' -print | etags - + +depend: + $(CC) $(DEPENDFLAGS) @DEFS@ $(INCLUDES) $(CFLAGS) -MM $(SRC) > .depend + +#include .depend + diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c new file mode 100644 index 00000000..2b4926eb --- /dev/null +++ b/datastore/text/clixon_xmldb_text.c @@ -0,0 +1,501 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include "clixon_xmldb_text.h" + +/*! Database locking for candidate and running non-persistent + * Store an integer for running and candidate containing + * the session-id of the client holding the lock. + */ +static int _running_locked = 0; +static int _candidate_locked = 0; +static int _startup_locked = 0; + +/*! Translate from symbolic database name to actual filename in file-system + * @param[in] h Clicon handle + * @param[in] db Symbolic database name, eg "candidate", "running" + * @param[out] filename Filename. Unallocate after use with free() + * @retval 0 OK + * @retval -1 Error + * @note Could need a way to extend which databases exists, eg to register new. + * The currently allowed databases are: + * candidate, tmp, running, result + * The filename reside in CLICON_XMLDB_DIR option + */ +static int +db2file(clicon_handle h, + char *db, + char **filename) +{ + int retval = -1; + cbuf *cb; + char *dir; + + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if ((dir = clicon_xmldb_dir(h)) == NULL){ + clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set"); + goto done; + } + if (strcmp(db, "running") != 0 && + strcmp(db, "candidate") != 0 && + strcmp(db, "startup") != 0 && + strcmp(db, "tmp") != 0){ + clicon_err(OE_XML, 0, "Unexpected database: %s", db); + goto done; + } + cprintf(cb, "%s/%s_db", dir, db); + if ((*filename = strdup4(cbuf_get(cb))) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + +/*! Get content of database using xpath. return a set of matching sub-trees + * The function returns a minimal tree that includes all sub-trees that match + * xpath. + * @param[in] dbname Name of database to search in (filename including dir path + * @param[in] xpath String with XPATH syntax. or NULL for all + * @param[in] yspec Yang specification + * @param[out] xtop Single XML tree which xvec points to. Free with xml_free() + * @param[out] xvec Vector of xml trees. Free after use. + * @param[out] xlen Length of vector. + * @retval 0 OK + * @retval -1 Error + * @code + * cxobj *xt; + * cxobj **xvec; + * size_t xlen; + * yang_spec *yspec = clicon_dbspec_yang(h); + * if (xmldb_get("running", "/interfaces/interface[name="eth"]", + * &xt, &xvec, &xlen) < 0) + * err; + * for (i=0; i17", &xt) < 0) + * err; + * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) + * err; + * @endcode + * @see xmldb_put_xkey for single key + */ +int +text_put(clicon_handle h, + char *db, + enum operation_type op, + char *api_path, + cxobj *xt) +{ + int retval = -1; + retval = 0; + // done: + return retval; +} + +/*! Raw dump of database, just keys and values, no xml interpretation + * @param[in] f File + * @param[in] dbfile File-name of database. This is a local file + * @param[in] rxkey Key regexp, eg "^.*$" + * @note This function can only be called locally. + */ +int +text_dump(FILE *f, + char *dbfilename, + char *rxkey) +{ + int retval = -1; + + retval = 0; + // done: + return retval; +} + +/*! Copy database from db1 to db2 + * @param[in] h Clicon handle + * @param[in] from Source database copy + * @param[in] to Destination database + * @retval -1 Error + * @retval 0 OK + */ +int +text_copy(clicon_handle h, + char *from, + char *to) +{ + int retval = -1; + retval = 0; + // done: + return retval; +} + +/*! Lock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + */ +int +text_lock(clicon_handle h, + char *db, + int pid) +{ + if (strcmp("running", db) == 0) + _running_locked = pid; + else if (strcmp("candidate", db) == 0) + _candidate_locked = pid; + else if (strcmp("startup", db) == 0) + _startup_locked = pid; + clicon_debug(1, "%s: locked by %u", db, pid); + return 0; +} + +/*! Unlock database + * @param[in] h Clicon handle + * @param[in] db Database + * @param[in] pid Process id + * @retval -1 Error + * @retval 0 OK + * Assume all sanity checks have been made + */ +int +text_unlock(clicon_handle h, + char *db, + int pid) +{ + if (strcmp("running", db) == 0) + _running_locked = 0; + else if (strcmp("candidate", db) == 0) + _candidate_locked = 0; + else if (strcmp("startup", db) == 0) + _startup_locked = 0; + return 0; +} + +/*! Unlock all databases locked by pid (eg process dies) + * @param[in] h Clicon handle + * @param[in] pid Process / Session id + * @retval -1 Error + * @retval 0 Ok + */ +int +text_unlock_all(clicon_handle h, + int pid) +{ + if (_running_locked == pid) + _running_locked = 0; + if (_candidate_locked == pid) + _candidate_locked = 0; + if (_startup_locked == pid) + _startup_locked = 0; + return 0; +} + +/*! Check if database is locked + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 Not locked + * @retval >0 Id of locker + */ +int +text_islocked(clicon_handle h, + char *db) +{ + if (strcmp("running", db) == 0) + return (_running_locked); + else if (strcmp("candidate", db) == 0) + return(_candidate_locked); + else if (strcmp("startup", db) == 0) + return(_startup_locked); + return 0; +} + +/*! Check if db exists + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 No it does not exist + * @retval 1 Yes it exists + */ +int +text_exists(clicon_handle h, + char *db) +{ + int retval = -1; + char *filename = NULL; + struct stat sb; + + if (db2file(h, db, &filename) < 0) + goto done; + if (lstat(filename, &sb) < 0) + retval = 0; + else + retval = 1; + done: + if (filename) + free(filename); + return retval; +} + +/*! Delete database. Remove file + * @param[in] h Clicon handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 OK + */ +int +text_delete(clicon_handle h, + char *db) +{ + int retval = -1; + + retval = 0; + // done: + return retval; +} + +/*! Initialize database + * @param[in] h Clicon handle + * @param[in] db Database + * @retval 0 OK + * @retval -1 Error + */ +int +text_init(clicon_handle h, + char *db) +{ + int retval = -1; + + retval = 0; + // done: + return retval; +} + +/*! plugin init function */ +int +text_plugin_exit(void) +{ + return 0; +} + +static const struct xmldb_api api; + +/*! plugin init function */ +void * +clixon_xmldb_plugin_init(int version) +{ + if (version != XMLDB_API_VERSION){ + clicon_err(OE_DB, 0, "Invalid version %d expected %d", + version, XMLDB_API_VERSION); + goto done; + } + return (void*)&api; + done: + return NULL; +} + +static const struct xmldb_api api = { + 1, + XMLDB_API_MAGIC, + clixon_xmldb_plugin_init, + text_plugin_exit, + text_get, + text_put, + text_dump, + text_copy, + text_lock, + text_unlock, + text_unlock_all, + text_islocked, + text_exists, + text_delete, + text_init, +}; + + +#if 0 /* Test program */ +/* + * Turn this on to get an xpath test program + * Usage: clicon_xpath [] + * read xml from input + * Example compile: + gcc -g -o xmldb -I. -I../clixon ./clixon_xmldb.c -lclixon -lcligen +*/ + +static int +usage(char *argv0) +{ + fprintf(stderr, "usage:\n%s\tget []\t\txml on stdin\n", argv0); + fprintf(stderr, "\tput set|merge|delete\txml to stdout\n"); + exit(0); +} + +int +main(int argc, char **argv) +{ + cxobj *xt; + cxobj *xn; + char *xpath; + enum operation_type op; + char *cmd; + char *db; + char *yangdir; + char *yangmod; + yang_spec *yspec = NULL; + clicon_handle h; + + if ((h = clicon_handle_init()) == NULL) + goto done; + clicon_log_init("xmldb", LOG_DEBUG, CLICON_LOG_STDERR); + if (argc < 4){ + usage(argv[0]); + goto done; + } + cmd = argv[1]; + db = argv[2]; + yangdir = argv[3]; + yangmod = argv[4]; + db_init(db); + if ((yspec = yspec_new()) == NULL) + goto done + if (yang_parse(h, yangdir, yangmod, NULL, yspec) < 0) + goto done; + if (strcmp(cmd, "get")==0){ + if (argc < 5) + usage(argv[0]); + xpath = argc>5?argv[5]:NULL; + if (xmldb_get(h, db, xpath, &xt, NULL, NULL) < 0) + goto done; + clicon_xml2file(stdout, xt, 0, 1); + } + else + if (strcmp(cmd, "put")==0){ + if (argc != 6) + usage(argv[0]); + if (clicon_xml_parse_file(0, &xt, "") < 0) + goto done; + if (xml_rootchild(xt, 0, &xn) < 0) + goto done; + if (strcmp(argv[5], "set") == 0) + op = OP_REPLACE; + else + if (strcmp(argv[4], "merge") == 0) + op = OP_MERGE; + else if (strcmp(argv[5], "delete") == 0) + op = OP_REMOVE; + else + usage(argv[0]); + if (xmldb_put(h, db, op, NULL, xn) < 0) + goto done; + } + else + usage(argv[0]); + printf("\n"); + done: + return 0; +} + +#endif /* Test program */ diff --git a/datastore/text/clixon_xmldb_text.h b/datastore/text/clixon_xmldb_text.h new file mode 100644 index 00000000..b2ca183d --- /dev/null +++ b/datastore/text/clixon_xmldb_text.h @@ -0,0 +1,56 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + Key-value store + */ +#ifndef _CLIXON_XMLDB_TEXT_H +#define _CLIXON_XMLDB_TEXT_H + +/* + * Prototypes + */ +int text_get(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen); +int text_put(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt); +int text_dump(FILE *f, char *dbfilename, char *rxkey); +int text_copy(clicon_handle h, char *from, char *to); +int text_lock(clicon_handle h, char *db, int pid); +int text_unlock(clicon_handle h, char *db, int pid); +int text_unlock_all(clicon_handle h, int pid); +int text_islocked(clicon_handle h, char *db); +int text_exists(clicon_handle h, char *db); +int text_delete(clicon_handle h, char *db); +int text_init(clicon_handle h, char *db); + +#endif /* _CLIXON_XMLDB_TEXT_H */ diff --git a/lib/clixon/clixon.h b/lib/clixon/clixon.h deleted file mode 100644 index 4ccdcab5..00000000 --- a/lib/clixon/clixon.h +++ /dev/null @@ -1,91 +0,0 @@ -/* lib/clixon/clixon.h. Generated from clixon.h.in by configure. */ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - - * Meta-include file that includes all sub-files in control-lib - * Note: this include files is for external purposes. Do not include this - * file in clicon lib-routines. - */ - -/* This include file requires the following include file dependencies */ -#include -#include -#include -#include -#include -#include - -/* - * CLIXON version macros - */ - -#define CLIXON_VERSION_STRING "3.2.0" -#define CLIXON_VERSION_MAJOR 3 -#define CLIXON_VERSION_MINOR 2 -#define CLIXON_VERSION_PATCH 0 - -/* - * Use this constant to disable some prototypes that should not be visible outside the lib. - * This is an alternative to use separate internal include files. - */ -#define LIBCLIXON_API 1 - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* - * Global variables generated by Makefile - */ -extern const char CLIXON_BUILDSTR[]; -extern const char CLIXON_VERSION[]; diff --git a/lib/clixon/clixon.h.in b/lib/clixon/clixon.h.in index 5d39d819..907aba4f 100644 --- a/lib/clixon/clixon.h.in +++ b/lib/clixon/clixon.h.in @@ -31,6 +31,9 @@ ***** END LICENSE BLOCK ***** + * NOTE: clixon.h is a GENERATED FILE and should not be edited. + * clixon.h.in is the original + * * Meta-include file that includes all sub-files in control-lib * Note: this include files is for external purposes. Do not include this * file in clicon lib-routines. @@ -67,13 +70,11 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include From 85af4342dcd3cdc45f382d21f5b6adf0e8ab7c87 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 12 Apr 2017 22:39:01 +0200 Subject: [PATCH 05/24] common plugin code, removed clixon_proc.c --- apps/backend/backend_plugin.c | 54 ++----- apps/cli/cli_plugin.c | 48 +----- apps/netconf/netconf_plugin.c | 54 ------- apps/restconf/restconf_lib.c | 61 +------ lib/clixon/clixon.h.in | 1 - lib/clixon/clixon_file.h | 2 + lib/clixon/clixon_plugin.h | 6 +- lib/clixon/clixon_proc.h | 46 ------ lib/src/Makefile.in | 2 +- lib/src/clixon_file.c | 32 ++++ lib/src/clixon_plugin.c | 74 +++++++++ lib/src/clixon_proc.c | 293 ---------------------------------- lib/src/clixon_string.c | 11 +- 13 files changed, 142 insertions(+), 542 deletions(-) delete mode 100644 lib/clixon/clixon_proc.h delete mode 100644 lib/src/clixon_proc.c diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 4889e6b5..7f0551b6 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -152,8 +152,8 @@ config_plugin_init(clicon_handle h) * @retval -1 Error */ static int -plugin_unload(clicon_handle h, - struct plugin *plg) +backend_plugin_unload(clicon_handle h, + struct plugin *plg) { char *error; @@ -182,37 +182,16 @@ plugin_unload(clicon_handle h, * @retval NULL Error */ static struct plugin * -plugin_load (clicon_handle h, - char *file, - int dlflags) +backend_plugin_load (clicon_handle h, + char *file, + int dlflags) { - char *error; void *handle; char *name; - struct plugin *new; - plginit_t *initfun; - - dlerror(); /* Clear any existing error */ - if ((handle = dlopen (file, dlflags)) == NULL) { - error = (char*)dlerror(); - clicon_err(OE_UNIX, 0, "dlopen: %s", error?error:"Unknown error"); - return NULL; - } - - initfun = dlsym(handle, PLUGIN_INIT); - if ((error = (char*)dlerror()) != NULL) { - clicon_err(OE_UNIX, 0, "dlsym: %s: %s", file, error); - return NULL; - } - - if (initfun(h) != 0) { - dlclose(handle); - if (!clicon_errno) /* sanity: log if clicon_err() is not called ! */ - clicon_err(OE_DB, 0, "Unknown error: %s: plugin_init does not make clicon_err call on error", - file); - return NULL; - } + struct plugin *new = NULL; + if ((handle = plugin_load(h, file, dlflags)) == NULL) + goto done; if ((new = malloc(sizeof(*new))) == NULL) { clicon_err(OE_UNIX, errno, "dhunk: %s", strerror(errno)); dlclose(handle); @@ -224,7 +203,6 @@ plugin_load (clicon_handle h, snprintf(new->p_name, sizeof(new->p_name), "%*s", (int)strlen(name)-2, name); new->p_handle = handle; - new->p_init = initfun; if ((new->p_start = dlsym(handle, PLUGIN_START)) != NULL) clicon_debug(2, "%s callback registered.", PLUGIN_START); if ((new->p_exit = dlsym(handle, PLUGIN_EXIT)) != NULL) @@ -244,7 +222,7 @@ plugin_load (clicon_handle h, if ((new->p_trans_abort = dlsym(handle, PLUGIN_TRANS_ABORT)) != NULL) clicon_debug(2, "%s callback registered.", PLUGIN_TRANS_ABORT); clicon_debug(2, "Plugin '%s' loaded.\n", name); - + done: return new; } @@ -338,7 +316,7 @@ plugin_append(struct plugin *p) * @retval -1 Error */ static int -config_plugin_load_dir(clicon_handle h, +backend_plugin_load_dir(clicon_handle h, const char *dir) { int retval = -1; @@ -374,7 +352,7 @@ config_plugin_load_dir(clicon_handle h, clicon_debug(1, "Loading master plugin '%.*s' ...", (int)strlen(filename), filename); - new = plugin_load(h, filename, RTLD_NOW|RTLD_GLOBAL); + new = backend_plugin_load(h, filename, RTLD_NOW|RTLD_GLOBAL); if (new == NULL) goto quit; if (plugin_append(new) < 0) @@ -387,7 +365,7 @@ config_plugin_load_dir(clicon_handle h, continue; /* Skip master now */ snprintf(filename, MAXPATHLEN-1, "%s/%s", dir, dp[i].d_name); clicon_debug(1, "Loading plugin '%.*s' ...", (int)strlen(filename), filename); - new = plugin_load(h, filename, RTLD_NOW); + new = backend_plugin_load(h, filename, RTLD_NOW); if (new == NULL) goto quit; /* Append to 'plugins' */ @@ -405,7 +383,7 @@ quit: while (--np >= 0){ if ((p = &plugins[np]) == NULL) continue; - plugin_unload(h, p); + backend_plugin_unload(h, p); free(p); } free(plugins); @@ -428,7 +406,7 @@ plugin_initiate(clicon_handle h) char *dir; /* First load CLICON system plugins */ - if (config_plugin_load_dir(h, CLIXON_BACKEND_SYSDIR) < 0) + if (backend_plugin_load_dir(h, CLIXON_BACKEND_SYSDIR) < 0) return -1; /* Then load application plugins */ @@ -436,7 +414,7 @@ plugin_initiate(clicon_handle h) clicon_err(OE_PLUGIN, 0, "backend_dir not defined"); return -1; } - if (config_plugin_load_dir(h, dir) < 0) + if (backend_plugin_load_dir(h, dir) < 0) return -1; return 0; @@ -455,7 +433,7 @@ plugin_finish(clicon_handle h) for (i = 0; i < nplugins; i++) { p = &plugins[i]; - plugin_unload(h, p); + backend_plugin_unload(h, p); } if (plugins){ free(plugins); diff --git a/apps/cli/cli_plugin.c b/apps/cli/cli_plugin.c index fdbe8408..77252d83 100644 --- a/apps/cli/cli_plugin.c +++ b/apps/cli/cli_plugin.c @@ -162,31 +162,6 @@ syntax_append(clicon_handle h, return 0; } -/* - * Unload a plugin - */ -static int -plugin_unload(clicon_handle h, void *handle) -{ - int retval = 0; - char *error; - plgexit_t *exitfun; - - /* Call exit function is it exists */ - exitfun = dlsym(handle, PLUGIN_EXIT); - if (dlerror() == NULL) - exitfun(h); - - dlerror(); /* Clear any existing error */ - if (dlclose(handle) != 0) { - error = (char*)dlerror(); - cli_output (stderr, "dlclose: %s\n", error ? error : "Unknown error"); - /* Just report */ - } - - return retval; -} - /* * Unload all plugins in a group */ @@ -275,41 +250,22 @@ cli_plugin_load(clicon_handle h, char *file, int dlflags) { - char *error; char *name; - void *handle = NULL; - plginit_t *initfun; + plghndl_t handle = NULL; struct cli_plugin *cp = NULL; - dlerror(); /* Clear any existing error */ - if ((handle = dlopen (file, dlflags)) == NULL) { - error = (char*)dlerror(); - cli_output (stderr, "dlopen: %s\n", error ? error : "Unknown error"); + if ((handle = plugin_load(h, file, dlflags)) == NULL) goto quit; - } - /* call plugin_init() if defined */ - if ((initfun = dlsym(handle, PLUGIN_INIT)) != NULL) { - if (initfun(h) != 0) { - cli_output (stderr, "Failed to initiate %s\n", strrchr(file,'/')?strchr(file, '/'):file); - goto quit; - } - } - if ((cp = malloc(sizeof (struct cli_plugin))) == NULL) { perror("malloc"); goto quit; } memset (cp, 0, sizeof(*cp)); - name = basename(file); snprintf(cp->cp_name, sizeof(cp->cp_name), "%.*s", (int)strlen(name)-3, name); cp->cp_handle = handle; quit: - if (cp == NULL) { - if (handle) - dlclose(handle); - } return cp; } diff --git a/apps/netconf/netconf_plugin.c b/apps/netconf/netconf_plugin.c index 9dc5d434..9a2e611f 100644 --- a/apps/netconf/netconf_plugin.c +++ b/apps/netconf/netconf_plugin.c @@ -75,60 +75,6 @@ struct netconf_reg { }; typedef struct netconf_reg netconf_reg_t; -/*! Unload a plugin - */ -static int -plugin_unload(clicon_handle h, void *handle) -{ - int retval = 0; - char *error; - plgexit_t *exitfn; - - /* Call exit function is it exists */ - exitfn = dlsym(handle, PLUGIN_EXIT); - if (dlerror() == NULL) - exitfn(h); - - dlerror(); /* Clear any existing error */ - if (dlclose(handle) != 0) { - error = (char*)dlerror(); - clicon_err(OE_PLUGIN, errno, "dlclose: %s\n", error ? error : "Unknown error"); - /* Just report */ - } - return retval; -} - - - -/* - * Load a dynamic plugin object and call it's init-function - * Note 'file' may be destructively modified - */ -static plghndl_t -plugin_load (clicon_handle h, char *file, int dlflags) -{ - char *error; - void *handle = NULL; - plginit_t *initfn; - - dlerror(); /* Clear any existing error */ - if ((handle = dlopen (file, dlflags)) == NULL) { - error = (char*)dlerror(); - clicon_err(OE_PLUGIN, errno, "dlopen: %s\n", error ? error : "Unknown error"); - goto quit; - } - /* call plugin_init() if defined */ - if ((initfn = dlsym(handle, PLUGIN_INIT)) != NULL) { - if (initfn(h) != 0) { - clicon_err(OE_PLUGIN, errno, "Failed to initiate %s\n", strrchr(file,'/')?strchr(file, '/'):file); - goto quit; - } - } -quit: - - return handle; -} - static int nplugins = 0; static plghndl_t *plugins = NULL; static netconf_reg_t *deps = NULL; diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index cd933a36..7f5f6e4d 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -269,44 +269,6 @@ static int nplugins = 0; static plghndl_t *plugins = NULL; static credentials_t *p_credentials = NULL; /* Credentials callback */ -/*! Load a dynamic plugin object and call it's init-function - * Note 'file' may be destructively modified - */ -static plghndl_t -plugin_load (clicon_handle h, - char *file, - int dlflags) -{ - char *error; - void *handle = NULL; - plginit_t *initfn; - - clicon_debug(1, "%s", __FUNCTION__); - dlerror(); /* Clear any existing error */ - if ((handle = dlopen (file, dlflags)) == NULL) { - error = (char*)dlerror(); - clicon_err(OE_PLUGIN, errno, "dlopen: %s\n", error ? error : "Unknown error"); - goto done; - } - /* call plugin_init() if defined */ - if ((initfn = dlsym(handle, PLUGIN_INIT)) == NULL){ - clicon_err(OE_PLUGIN, errno, "Failed to find plugin_init when loading restconf plugin %s", file); - goto err; - } - if (initfn(h) != 0) { - clicon_err(OE_PLUGIN, errno, "Failed to initiate %s", strrchr(file,'/')?strchr(file, '/'):file); - goto err; - } - p_credentials = dlsym(handle, "restconf_credentials"); - done: - return handle; - err: - if (handle) - dlclose(handle); - return NULL; -} - - /*! Load all plugins you can find in CLICON_RESTCONF_DIR */ int @@ -335,6 +297,7 @@ restconf_plugin_load(clicon_handle h) (int)strlen(filename), filename); if ((handle = plugin_load(h, filename, RTLD_NOW)) == NULL) goto quit; + p_credentials = dlsym(handle, "restconf_credentials"); if ((plugins = realloc(plugins, (nplugins+1) * sizeof (*plugins))) == NULL) { clicon_err(OE_UNIX, errno, "realloc"); goto quit; @@ -348,28 +311,6 @@ quit: return retval; } -/*! Unload a plugin - */ -static int -plugin_unload(clicon_handle h, void *handle) -{ - int retval = 0; - char *error; - plgexit_t *exitfn; - - /* Call exit function is it exists */ - exitfn = dlsym(handle, PLUGIN_EXIT); - if (dlerror() == NULL) - exitfn(h); - - dlerror(); /* Clear any existing error */ - if (dlclose(handle) != 0) { - error = (char*)dlerror(); - clicon_err(OE_PLUGIN, errno, "dlclose: %s\n", error ? error : "Unknown error"); - /* Just report */ - } - return retval; -} /*! Unload all restconf plugins */ int diff --git a/lib/clixon/clixon.h.in b/lib/clixon/clixon.h.in index 907aba4f..d9f00a80 100644 --- a/lib/clixon/clixon.h.in +++ b/lib/clixon/clixon.h.in @@ -76,7 +76,6 @@ #include #include #include -#include #include #include #include diff --git a/lib/clixon/clixon_file.h b/lib/clixon/clixon_file.h index 5965f62b..08cc1f57 100644 --- a/lib/clixon/clixon_file.h +++ b/lib/clixon/clixon_file.h @@ -42,4 +42,6 @@ int clicon_file_dirent(const char *dir, struct dirent **ent, int clicon_file_copy(char *src, char *target); +int group_name2gid(char *name, gid_t *gid); + #endif /* _CLIXON_FILE_H_ */ diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index 7dc50ddf..10d3d082 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -51,7 +51,7 @@ typedef void *(find_plugin_t)(clicon_handle, char *); * Prototypes */ /* Common plugin function names, function types and signatures. - * This set of plugins is extended in + * This plugin code is exytended by backend, cli, netconf, restconf plugins * Cli see cli_plugin.c * Backend see config_plugin.c */ @@ -77,4 +77,8 @@ typedef int (plgexit_t)(clicon_handle); /* Plugin exit */ /* Find a function in global namespace or a plugin. XXX clicon internal */ void *clicon_find_func(clicon_handle h, char *plugin, char *func); +plghndl_t plugin_load (clicon_handle h, char *file, int dlflags); + +int plugin_unload(clicon_handle h, plghndl_t *handle); + #endif /* _CLIXON_PLUGIN_H_ */ diff --git a/lib/clixon/clixon_proc.h b/lib/clixon/clixon_proc.h deleted file mode 100644 index 2a85d9b0..00000000 --- a/lib/clixon/clixon_proc.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - - */ - -#ifndef _CLIXON_PROC_H_ -#define _CLIXON_PROC_H_ - -/* - * Prototypes - */ -int clicon_proc_run (char *, void (outcb)(char *), int doerr); -int clicon_proc_daemon (char *); -int group_name2gid(char *name, gid_t *gid); - -#endif /* _CLIXON_PROC_H_ */ diff --git a/lib/src/Makefile.in b/lib/src/Makefile.in index 0135d78e..8c390233 100644 --- a/lib/src/Makefile.in +++ b/lib/src/Makefile.in @@ -61,7 +61,7 @@ CPPFLAGS = @CPPFLAGS@ INCLUDES = -I. @INCLUDES@ -I$(top_srcdir)/lib/clixon -I$(top_srcdir)/include -I$(top_srcdir) -SRC = clixon_sig.c clixon_log.c clixon_err.c clixon_event.c clixon_proc.c \ +SRC = clixon_sig.c clixon_log.c clixon_err.c clixon_event.c \ clixon_string.c clixon_handle.c \ clixon_xml.c clixon_xml_map.c clixon_file.c \ clixon_json.c clixon_yang.c clixon_yang_type.c \ diff --git a/lib/src/clixon_file.c b/lib/src/clixon_file.c index 082ee6ae..bb1868dd 100644 --- a/lib/src/clixon_file.c +++ b/lib/src/clixon_file.c @@ -51,6 +51,7 @@ #include #include #include +#include /* cligen */ #include @@ -229,3 +230,34 @@ clicon_file_copy(char *src, } +/*! Translate group name to gid. Return -1 if error or not found. + * @param[in] name Name of group + * @param[out] gid Group id + * @retval 0 OK + * @retval -1 Error. or not found + */ +int +group_name2gid(char *name, + gid_t *gid) +{ + char buf[1024]; + struct group g0; + struct group *gr = &g0; + struct group *gtmp; + + gr = &g0; + /* This leaks memory in ubuntu */ + if (getgrnam_r(name, gr, buf, sizeof(buf), >mp) < 0){ + clicon_err(OE_UNIX, errno, "%s: getgrnam_r(%s): %s", + __FUNCTION__, name, strerror(errno)); + return -1; + } + if (gtmp == NULL){ + clicon_err(OE_UNIX, 0, "%s: No such group: %s", __FUNCTION__, name); + fprintf(stderr, "No such group %s\n", name); + return -1; + } + if (gid) + *gid = gr->gr_gid; + return 0; +} diff --git a/lib/src/clixon_plugin.c b/lib/src/clixon_plugin.c index 24a5319b..c127a948 100644 --- a/lib/src/clixon_plugin.c +++ b/lib/src/clixon_plugin.c @@ -45,6 +45,7 @@ #include "clixon_err.h" #include "clixon_queue.h" #include "clixon_hash.h" +#include "clixon_log.h" #include "clixon_handle.h" #include "clixon_plugin.h" @@ -84,3 +85,76 @@ clicon_find_func(clicon_handle h, char *plugin, char *func) return dlsym(dlhandle, func); } + +/*! Load a dynamic plugin object and call its init-function + * Note 'file' may be destructively modified + * @param[in] h Clicon handle + * @param[in] file Which plugin to load + * @param[in] dlflags See man(3) dlopen + */ +plghndl_t +plugin_load (clicon_handle h, + char *file, + int dlflags) +{ + char *error; + void *handle = NULL; + plginit_t *initfn; + + clicon_debug(1, "%s", __FUNCTION__); + dlerror(); /* Clear any existing error */ + if ((handle = dlopen (file, dlflags)) == NULL) { + error = (char*)dlerror(); + clicon_err(OE_PLUGIN, errno, "dlopen: %s\n", error ? error : "Unknown error"); + goto done; + } + /* call plugin_init() if defined */ + if ((initfn = dlsym(handle, PLUGIN_INIT)) == NULL){ + clicon_err(OE_PLUGIN, errno, "Failed to find plugin_init when loading restconf plugin %s", file); + goto err; + } + if ((error = (char*)dlerror()) != NULL) { + clicon_err(OE_UNIX, 0, "dlsym: %s: %s", file, error); + goto done; + } + if (initfn(h) != 0) { + clicon_err(OE_PLUGIN, errno, "Failed to initiate %s", strrchr(file,'/')?strchr(file, '/'):file); + if (!clicon_errno) /* sanity: log if clicon_err() is not called ! */ + clicon_err(OE_DB, 0, "Unknown error: %s: plugin_init does not make clicon_err call on error", + file); + goto err; + } + done: + return handle; + err: + if (handle) + dlclose(handle); + return NULL; +} + + +/*! Unload a plugin + * @param[in] h Clicon handle + * @param[in] handle Clicon handle + */ +int +plugin_unload(clicon_handle h, + plghndl_t *handle) +{ + int retval = 0; + char *error; + plgexit_t *exitfn; + + /* Call exit function is it exists */ + exitfn = dlsym(handle, PLUGIN_EXIT); + if (dlerror() == NULL) + exitfn(h); + + dlerror(); /* Clear any existing error */ + if (dlclose(handle) != 0) { + error = (char*)dlerror(); + clicon_err(OE_PLUGIN, errno, "dlclose: %s\n", error ? error : "Unknown error"); + /* Just report */ + } + return retval; +} diff --git a/lib/src/clixon_proc.c b/lib/src/clixon_proc.c deleted file mode 100644 index cd1e363b..00000000 --- a/lib/src/clixon_proc.c +++ /dev/null @@ -1,293 +0,0 @@ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - - */ - -#ifdef HAVE_CONFIG_H -#include "clixon_config.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* clicon */ -#include "clixon_err.h" -#include "clixon_log.h" -#include "clixon_sig.h" -#include "clixon_string.h" -#include "clixon_queue.h" -#include "clixon_proc.h" - -/* - * Macros - */ -#define signal_set_mask(set) sigprocmask(SIG_SETMASK, (set), NULL) -#define signal_get_mask(set) sigprocmask (0, NULL, (set)) - -/* - * Child process ID - * XXX Really shouldn't be a global variable - */ -static int _clicon_proc_child = 0; - -/* - * Make sure child is killed by ctrl-C - */ -static void -clicon_proc_sigint(int sig) -{ - if (_clicon_proc_child > 0) - kill (_clicon_proc_child, SIGINT); -} - -/*! Fork a child process, setup a pipe between parent and child. - * Allowing parent to read the output of the child. - * @param[in] doerr If non-zero, stderr will be directed to the pipe as well. - * The pipe for the parent to write - * to the child is closed and cannot be used. - * - * When child process is done with the pipe setup, execute the specified - * command, execv(argv[0], argv). - * - * When parent is done with the pipe setup it will read output from the child - * until eof. The read output will be sent to the specified output callback, - * 'outcb' function. - * - * @retval number Matches (processes affected). - * @retval -1 Error. - */ -int -clicon_proc_run (char *cmd, - void (outcb)(char *), - int doerr) -{ - char - **argv, - buf[512]; - int - outfd[2] = { -1, -1 }; - int - n, - argc, - status, - retval = -1; - pid_t - child; - sigfn_t oldhandler = NULL; - sigset_t oset; - - argv = clicon_strsep(cmd, " \t", &argc); - if (!argv) - return -1; - - if (pipe (outfd) == -1) - goto done; - - signal_get_mask(&oset); - set_signal(SIGINT, clicon_proc_sigint, &oldhandler); - - - if ((child = fork ()) < 0) { - retval = -1; - goto done; - } - - if (child == 0) { /* Child */ - - /* Unblock all signals except TSTP */ - clicon_signal_unblock (0); - signal (SIGTSTP, SIG_IGN); - - close (outfd[0]); /* Close unused read ends */ - outfd[0] = -1; - - /* Divert stdout and stderr to pipes */ - dup2 (outfd[1], STDOUT_FILENO); - if (doerr) - dup2 (outfd[1], STDERR_FILENO); - - execvp (argv[0], argv); - perror("execvp"); - _exit(-1); - } - - /* Parent */ - - /* Close unused write ends */ - close (outfd[1]); - outfd[1] = -1; - - /* Read from pipe */ - while ((n = read (outfd[0], buf, sizeof (buf)-1)) != 0) { - if (n < 0) { - if (errno == EINTR) - continue; - break; - } - buf[n] = '\0'; - /* Pass read data to callback function is defined */ - if (outcb) - outcb (buf); - } - - /* Wait for child to finish */ - if(waitpid (child, &status, 0) == child) - retval = WEXITSTATUS(status); - else - retval = -1; - - done: - - /* Clean up all pipes */ - if (outfd[0] != -1) - close (outfd[0]); - if (outfd[1] != -1) - close (outfd[1]); - - /* Restore sigmask and fn */ - signal_set_mask (&oset); - set_signal(SIGINT, oldhandler, NULL); - - if(argv) - free(argv); - return retval; -} - -/*! Spawn command and report exit status - */ -int -clicon_proc_daemon (char *cmd) -{ - char - **argv; - int - i, - argc, - retval = -1, - status, status2; - pid_t - child, - pid; - struct rlimit - rlim; - - argv = clicon_strsep(cmd, " \t", &argc); - if (!argv) - return -1; - - if ((child = fork ()) < 0) { - clicon_err(OE_UNIX, errno, "fork"); - goto done; - } - - if (child == 0) { /* Child */ - - clicon_signal_unblock (0); - if ((pid = fork ()) < 0) { - clicon_err(OE_UNIX, errno, "fork"); - return -1; - _exit(1); - } - if (pid == 0) { /* Grandchild, create new session */ - setsid(); - if (chdir("/") < 0){ - clicon_err(OE_UNIX, errno, "chdirq"); - _exit(1); - } - /* Close open descriptors */ - if ( ! getrlimit (RLIMIT_NOFILE, &rlim)) - for (i = 0; i < rlim.rlim_cur; i++) - close(i); - - if (execv (argv[0], argv) < 0) { - clicon_err(OE_UNIX, errno, "execv"); - _exit(1); - } - /* Not reached */ - } - - waitpid (pid, &status2, 0); - _exit(status2); - - } - - if (waitpid (child, &status, 0) > 0) - retval = 0; - - done: - if (argv) - free(argv); - return (retval); -} - - -/*! Translate group name to gid. Return -1 if error or not found. - */ -int -group_name2gid(char *name, gid_t *gid) -{ - char buf[1024]; - struct group g0; - struct group *gr = &g0; - struct group *gtmp; - - gr = &g0; - /* This leaks memory in ubuntu */ - if (getgrnam_r(name, gr, buf, sizeof(buf), >mp) < 0){ - clicon_err(OE_UNIX, errno, "%s: getgrnam_r(%s): %s", - __FUNCTION__, name, strerror(errno)); - return -1; - } - if (gtmp == NULL){ - clicon_err(OE_UNIX, 0, "%s: No such group: %s", __FUNCTION__, name); - fprintf(stderr, "No such group %s\n", name); - return -1; - } - if (gid) - *gid = gr->gr_gid; - return 0; -} diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index aecb7977..98de980f 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -170,13 +170,16 @@ percent_encode(char *str, { int retval = -1; char *esc = NULL; + int len; int i, j; /* This is max */ - if ((esc = malloc(strlen(str)*3+1)) == NULL){ + len = strlen(str)*3+1; + if ((esc = malloc(len)) == NULL){ clicon_err(OE_UNIX, errno, "malloc"); goto done; } + memset(esc, 0, len); j = 0; for (i=0; i 2 && @@ -226,6 +232,7 @@ percent_decode(char *esc, str[j] = esc[i]; j++; } + str[j++] = '\0'; *strp = str; retval = 0; done: From d26a801bc0a9b24ba6469ab5cfd2bdcde03b0fd2 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 15 Apr 2017 13:53:58 +0200 Subject: [PATCH 06/24] Added connect/disconnect/getopt/setopt and handle to xmldb API; Added datastore 'text'; Moved apps/dbctrl to datastore/ --- CHANGELOG.txt | 4 + apps/Makefile.in | 1 - apps/backend/backend_handle.h | 2 +- apps/backend/backend_main.c | 34 +- apps/backend/clixon_backend_handle.c | 69 ++-- apps/cli/cli_handle.c | 8 +- apps/dbctrl/Makefile.in | 108 ----- apps/dbctrl/dbctrl_main.c | 241 ----------- configure | 5 +- configure.ac | 1 - datastore/Makefile.in | 51 ++- datastore/datastore_client.c | 275 +++++++++++++ datastore/keyvalue/Makefile.in | 1 + datastore/keyvalue/clixon_keyvalue.c | 572 ++++++++++++--------------- datastore/keyvalue/clixon_keyvalue.h | 20 +- datastore/text/clixon_xmldb_text.c | 334 ++++++++++++---- datastore/text/clixon_xmldb_text.h | 20 +- lib/clixon/clixon_handle.h | 6 + lib/clixon/clixon_xml_db.h | 64 ++- lib/clixon/clixon_xml_map.h | 4 + lib/src/clixon_handle.c | 37 +- lib/src/clixon_xml.c | 44 ++- lib/src/clixon_xml_db.c | 329 ++++++++++++--- lib/src/clixon_xml_map.c | 173 ++++++++ lib/src/clixon_yang.c | 10 +- 25 files changed, 1501 insertions(+), 912 deletions(-) delete mode 100644 apps/dbctrl/Makefile.in delete mode 100644 apps/dbctrl/dbctrl_main.c create mode 100644 datastore/datastore_client.c diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6b28a139..993727db 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -29,6 +29,10 @@ # # ***** END LICENSE BLOCK ***** +- Moved apps/dbctrl to datastore/ + +- Added connect/disconnect/getopt/setopt and handle to xmldb API + - Added datastore 'text' - Configure (autoconf) changes diff --git a/apps/Makefile.in b/apps/Makefile.in index 7f7ca9c0..a67233bc 100644 --- a/apps/Makefile.in +++ b/apps/Makefile.in @@ -43,7 +43,6 @@ SHELL = /bin/sh SUBDIRS = backend SUBDIRS += cli -SUBDIRS += dbctrl SUBDIRS += netconf ifeq ($(with_restconf),yes) SUBDIRS += restconf diff --git a/apps/backend/backend_handle.h b/apps/backend/backend_handle.h index 80251f08..4a81af16 100644 --- a/apps/backend/backend_handle.h +++ b/apps/backend/backend_handle.h @@ -41,7 +41,7 @@ * Prototypes * not exported. */ -/* backend handles */ +/* backend handles. Defined in clixon_backend_handle.c */ clicon_handle backend_handle_init(void); int backend_handle_exit(clicon_handle h); diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index f417e2ec..badf84f2 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -254,19 +254,20 @@ server_socket(clicon_handle h) * log event. */ static int -config_log_cb(int level, char *msg, void *arg) +config_log_cb(int level, + char *msg, + void *arg) { + int retval = -1; size_t n; - char *ptr; - char *nptr; - char *newmsg = NULL; - int retval = -1; + char *ptr; + char *nptr; + char *newmsg = NULL; - /* backend_notify() will go through all clients and see if any has registered "CLICON", - and if so make a clicon_proto notify message to those clients. */ - - - /* Sanitize '%' into "%%" to prevent segvfaults in vsnprintf later. + /* backend_notify() will go through all clients and see if any has + registered "CLICON", and if so make a clicon_proto notify message to + those clients. + Sanitize '%' into "%%" to prevent segvfaults in vsnprintf later. At this stage all formatting is already done */ n = 0; for(ptr=msg; *ptr; ptr++) @@ -281,7 +282,6 @@ config_log_cb(int level, char *msg, void *arg) if (*ptr == '%') *nptr++ = '%'; } - retval = backend_notify(arg, "CLICON", level, newmsg); free(newmsg); @@ -509,13 +509,21 @@ main(int argc, char **argv) clicon_log(LOG_ERR, "No xmldb plugin given (specify option CLICON_XMLDB_PLUGIN).\n"); goto done; } - if (xmldb_plugin_load(xmldb_plugin) < 0) + if (xmldb_plugin_load(h, xmldb_plugin) < 0) + goto done; + /* Connect to plugin to get a handle */ + if (xmldb_connect(h) < 0) goto done; - /* Parse db spec file */ if (yang_spec_main(h, stdout, printspec) < 0) goto done; + /* Set options: database dir aqnd yangspec (could be hidden in connect?)*/ + if (xmldb_setopt(h, "dbdir", clicon_xmldb_dir(h)) < 0) + goto done; + if (xmldb_setopt(h, "yangspec", clicon_dbspec_yang(h)) < 0) + goto done; + /* First check for startup config XXX the options below have become out-of-hand. Too complex, need to simplify*/ diff --git a/apps/backend/clixon_backend_handle.c b/apps/backend/clixon_backend_handle.c index 720337e8..85610654 100644 --- a/apps/backend/clixon_backend_handle.c +++ b/apps/backend/clixon_backend_handle.c @@ -75,14 +75,21 @@ * This file should only contain access functions for the _specific_ * entries in the struct below. */ +/*! Backend specific handle added to header CLICON handle + * This file should only contain access functions for the _specific_ + * entries in the struct below. + * @note The top part must be equivalent to struct clicon_handle in clixon_handle.c + * @see struct clicon_handle, struct cli_handle + */ struct backend_handle { - int cb_magic; /* magic (HDR)*/ - clicon_hash_t *cb_copt; /* clicon option list (HDR) */ - clicon_hash_t *cb_data; /* internal clicon data (HDR) */ + int bh_magic; /* magic (HDR)*/ + clicon_hash_t *bh_copt; /* clicon option list (HDR) */ + clicon_hash_t *bh_data; /* internal clicon data (HDR) */ + void *bh_xmldb; /* XMLDB storage handle, uie xmldb_handle */ /* ------ end of common handle ------ */ - struct client_entry *cb_ce_list; /* The client list */ - int cb_ce_nr; /* Number of clients, just increment */ - struct handle_subscription *cb_subscription; /* Event subscription list */ + struct client_entry *bh_ce_list; /* The client list */ + int bh_ce_nr; /* Number of clients, just increment */ + struct handle_subscription *bh_subscription; /* Event subscription list */ }; /*! Creates and returns a clicon config handle for other CLICON API calls @@ -257,11 +264,17 @@ backend_notify_xml(clicon_handle h, } +/*! Add new client, typically frontend such as cli, netconf, restconf + * @param[in] h Clicon handle + * @param[in] addr Address of client + * @retval ce Client entry + * @retval NULL Error + */ struct client_entry * backend_client_add(clicon_handle h, struct sockaddr *addr) { - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); struct client_entry *ce; if ((ce = (struct client_entry *)malloc(sizeof(*ce))) == NULL){ @@ -269,24 +282,28 @@ backend_client_add(clicon_handle h, return NULL; } memset(ce, 0, sizeof(*ce)); - ce->ce_nr = cb->cb_ce_nr++; + ce->ce_nr = bh->bh_ce_nr++; memcpy(&ce->ce_addr, addr, sizeof(*addr)); - ce->ce_next = cb->cb_ce_list; - cb->cb_ce_list = ce; + ce->ce_next = bh->bh_ce_list; + bh->bh_ce_list = ce; return ce; } +/*! Return client list + * @param[in] h Clicon handle + * @retval ce_list Client entry list (all sessions) + */ struct client_entry * backend_client_list(clicon_handle h) { - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); - return cb->cb_ce_list; + return bh->bh_ce_list; } /*! Actually remove client from client list * @param[in] h Clicon handle - * @param[in] ce Client hadnle + * @param[in] ce Client handle * @see backend_client_rm which is more high-level */ int @@ -295,9 +312,9 @@ backend_client_delete(clicon_handle h, { struct client_entry *c; struct client_entry **ce_prev; - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); - ce_prev = &cb->cb_ce_list; + ce_prev = &bh->bh_ce_list; for (c = *ce_prev; c; c = c->ce_next){ if (c == ce){ *ce_prev = c->ce_next; @@ -328,7 +345,7 @@ subscription_add(clicon_handle h, subscription_fn_t fn, void *arg) { - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); struct handle_subscription *hs = NULL; if ((hs = malloc(sizeof(*hs))) == NULL){ @@ -339,10 +356,10 @@ subscription_add(clicon_handle h, hs->hs_stream = strdup(stream); hs->hs_format = format; hs->hs_filter = filter?strdup(filter):NULL; - hs->hs_next = cb->cb_subscription; + hs->hs_next = bh->bh_subscription; hs->hs_fn = fn; hs->hs_arg = arg; - cb->cb_subscription = hs; + bh->bh_subscription = hs; done: return hs; } @@ -362,11 +379,11 @@ subscription_delete(clicon_handle h, subscription_fn_t fn, void *arg) { - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); struct handle_subscription *hs; struct handle_subscription **hs_prev; - hs_prev = &cb->cb_subscription; /* this points to stack and is not real backpointer */ + hs_prev = &bh->bh_subscription; /* this points to stack and is not real backpointer */ for (hs = *hs_prev; hs; hs = hs->hs_next){ /* XXX arg == hs->hs_arg */ if (strcmp(hs->hs_stream, stream)==0 && hs->hs_fn == fn){ @@ -404,15 +421,16 @@ struct handle_subscription * subscription_each(clicon_handle h, struct handle_subscription *hprev) { - struct backend_handle *cb = handle(h); + struct backend_handle *bh = handle(h); struct handle_subscription *hs = NULL; if (hprev) hs = hprev->hs_next; else - hs = cb->cb_subscription; + hs = bh->bh_subscription; return hs; } + /* Database dependency description */ struct backend_netconf_reg { qelem_t nr_qelem; /* List header */ @@ -456,10 +474,9 @@ catch: /*! See if there is any callback registered for this tag * * @param[in] h clicon handle - * @param[in] xn Sub-tree (under xorig) at child of rpc: . - * @param[out] cb Output xml stream. For reply - * @param[out] cb_err Error xml stream. For error reply - * @param[out] xret Return XML, error or OK + * @param[in] xe Sub-tree (under xorig) at child of rpc: . + * @param[in] ce Client (session) entry + * @param[out] cbret Return XML, error or OK as cbuf * * @retval -1 Error * @retval 0 OK, not found handler. diff --git a/apps/cli/cli_handle.c b/apps/cli/cli_handle.c index 62c96c11..bc63a179 100644 --- a/apps/cli/cli_handle.c +++ b/apps/cli/cli_handle.c @@ -69,17 +69,17 @@ #define handle(h) (assert(clicon_handle_check(h)==0),(struct cli_handle *)(h)) #define cligen(h) (handle(h)->cl_cligen) -/* - * cli_handle - * first part of this is header, same for clicon_handle and config_handle. - * Access functions for common fields are found in clicon lib: clicon_options.[ch] +/*! CLI specific handle added to header CLICON handle * This file should only contain access functions for the _specific_ * entries in the struct below. + * @note The top part must be equivalent to struct clicon_handle in clixon_handle.c + * @see struct clicon_handle, struct backend_handle */ struct cli_handle { int cl_magic; /* magic (HDR)*/ clicon_hash_t *cl_copt; /* clicon option list (HDR) */ clicon_hash_t *cl_data; /* internal clicon data (HDR) */ + void *cl_xmldb; /* XMLDB storage handle, uie xmldb_handle */ /* ------ end of common handle ------ */ cligen_handle cl_cligen; /* cligen handle */ diff --git a/apps/dbctrl/Makefile.in b/apps/dbctrl/Makefile.in deleted file mode 100644 index 2041e7d7..00000000 --- a/apps/dbctrl/Makefile.in +++ /dev/null @@ -1,108 +0,0 @@ -# -# ***** BEGIN LICENSE BLOCK ***** -# -# Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren -# -# This file is part of CLIXON -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Alternatively, the contents of this file may be used under the terms of -# the GNU General Public License Version 3 or later (the "GPL"), -# in which case the provisions of the GPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of the GPL, and not to allow others to -# use your version of this file under the terms of Apache License version 2, -# indicate your decision by deleting the provisions above and replace them with -# the notice and other provisions required by the GPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the Apache License version 2 or the GPL. -# -# ***** END LICENSE BLOCK ***** -# -VPATH = @srcdir@ -srcdir = @srcdir@ -top_srcdir = @top_srcdir@ -CC = @CC@ -CFLAGS = @CFLAGS@ -LDFLAGS = @LDFLAGS@ - -prefix = @prefix@ -exec_prefix = @exec_prefix@ -bindir = @bindir@ -libexecdir = @libexecdir@ -localstatedir = @localstatedir@ -sysconfdir = @sysconfdir@ - -SH_SUFFIX = @SH_SUFFIX@ -CLIXON_MAJOR = @CLIXON_VERSION_MAJOR@ -CLIXON_MINOR = @CLIXON_VERSION_MINOR@ - -# Use this clixon lib for linking -CLIXON_LIB = libclixon.so.$(CLIXON_MAJOR).$(CLIXON_MINOR) - -# For dependency -LIBDEPS = $(top_srcdir)/lib/src/$(CLIXON_LIB) - -LIBS = -L$(top_srcdir)/lib/src @LIBS@ -l:$(CLIXON_LIB) - -CPPFLAGS = @CPPFLAGS@ - -INCLUDES = -I. -I$(top_srcdir)/lib/src -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ - -SRC = - -OBJS = $(SRC:.c=.o) - -APPSRC = dbctrl_main.c -APPOBJ = $(APPSRC:.c=.o) -APPL = clixon_dbctrl - -all: $(APPL) - -clean: - rm -f $(OBJS) *.core $(APPL) $(APPOBJ) - -distclean: clean - rm -f Makefile *~ .depend - -# Put demon in bin -# Put other executables in libexec/ -# Also create a libexec/ directory for writeable/temporary files. -# Put config file in etc/ -install: $(APPL) - install -d $(DESTDIR)$(bindir) - install $(APPL) $(DESTDIR)$(bindir) - -install-include: - -uninstall: - rm -f $(bindir)/$(APPL) - -.SUFFIXES: -.SUFFIXES: .c .o - -.c.o: - $(CC) $(INCLUDES) -D__PROGRAM__=\"$(APPL)\" $(CPPFLAGS) $(CFLAGS) -c $< - -$(APPL) : $(APPOBJ) $(OBJS) $(LIBDEPS) - $(CC) $(LDFLAGS) $(APPOBJ) $(OBJS) $(LIBS) -o $@ - -TAGS: - find . -name '*.[chyl]' -print | etags - - -depend: - $(CC) $(DEPENDFLAGS) @DEFS@ $(INCLUDES) $(CFLAGS) -MM $(SRC) $(APPSRC) > .depend - -#include .depend - diff --git a/apps/dbctrl/dbctrl_main.c b/apps/dbctrl/dbctrl_main.c deleted file mode 100644 index 4910b958..00000000 --- a/apps/dbctrl/dbctrl_main.c +++ /dev/null @@ -1,241 +0,0 @@ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - - */ - -#ifdef HAVE_CONFIG_H -#include "clixon_config.h" /* generated by config & autoconf */ -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* cligen */ -#include - -/* clicon */ -#include - -/* Command line options to be passed to getopt(3) */ -#define DBCTRL_OPTS "hDSd:pbn:r:m:Zi" - -/* - * remove_entry - */ -static int -remove_entry(char *dbname, char *key) -{ -#ifdef NOTYET /* This assumes direct access to database */ - return db_del(dbname, key); -#else - return 0; -#endif -} - -/*! usage - */ -static void -usage(char *argv0) -{ - fprintf(stderr, "usage:%s\n" - "where options are\n" - "\t-h\t\tHelp\n" - "\t-D\t\tDebug\n" - "\t-S\t\tLog on syslog\n" - "\t-d \t\tDatabase name (default: running)\n" - "\t-p\t\tDump database on stdout\n" - "\t-b\t\tBrief output, just print keys. Combine with -p or -m\n" - "\t-n \" \" Add database entry\n" - "\t-r \tRemove database entry\n" - "\t-m \tMatch regexp key in database\n" - "\t-Z\t\tDelete database\n" - "\t-i\t\tInit database\n", - argv0 - ); - exit(0); -} - -int -main(int argc, char **argv) -{ - char c; - int zapdb; - int initdb; - int dumpdb; - int addent; - int rment; - char *matchkey = NULL; - char *addstr; - char rmkey[MAXPATHLEN]; - int brief; - char db[MAXPATHLEN] = {0,}; - int use_syslog; - clicon_handle h; - - /* In the startup, logs to stderr & debug flag set later */ - clicon_log_init(__PROGRAM__, LOG_INFO, CLICON_LOG_STDERR); - - /* Defaults */ - zapdb = 0; - initdb = 0; - dumpdb = 0; - addent = 0; - rment = 0; - brief = 0; - use_syslog = 0; - addstr = NULL; - memcpy(db, "running", strlen("running")+1); - memset(rmkey, '\0', sizeof(rmkey)); - - if ((h = clicon_handle_init()) == NULL) - goto done; - /* getopt in two steps, first find config-file before over-riding options. */ - while ((c = getopt(argc, argv, DBCTRL_OPTS)) != -1) - switch (c) { - case '?' : - case 'h' : /* help */ - usage(argv[0]); - break; - case 'D' : /* debug */ - debug = 1; - break; - case 'S': /* Log on syslog */ - use_syslog = 1; - break; - } - /* - * Logs, error and debug to stderr or syslog, set debug level - */ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, - use_syslog?CLICON_LOG_SYSLOG:CLICON_LOG_STDERR); - clicon_debug_init(debug, NULL); - - - /* Now rest of options */ - optind = 1; - while ((c = getopt(argc, argv, DBCTRL_OPTS)) != -1) - switch (c) { - case 'Z': /* Zap database */ - zapdb++; - break; - case 'i': /* Init database */ - initdb++; - break; - case 'p': /* Dump/print database */ - dumpdb++; - break; - case 'b': /* Dump/print/match database brief (combone w -p or -m) */ - brief++; - break; - case 'd': /* db either db filename or symbolic: running|candidate */ - if (!optarg || sscanf(optarg, "%s", db) != 1) - usage(argv[0]); - break; - case 'n': /* add database entry */ - if (!optarg || !strlen(optarg) || (addstr = strdup(optarg)) == NULL) - usage(argv[0]); - /* XXX addign both key and value, for now only key */ - addent++; - break; - case 'r': - if (!optarg || sscanf(optarg, "%s", rmkey) != 1) - usage(argv[0]); - rment++; - break; - case 'm': - if (!optarg || !strlen(optarg) || (matchkey = strdup(optarg)) == NULL) - usage(argv[0]); - dumpdb++; - break; - case 'D': /* Processed earlier, ignore now. */ - case 'S': - break; - default: - usage(argv[0]); - break; - } - argc -= optind; - argv += optind; - - if (*db == '\0'){ - clicon_err(OE_FATAL, 0, "database not specified (with -d ): %s"); - goto done; - } - if (dumpdb){ - /* Here db must be local file-path */ - if (xmldb_dump(stdout, db, matchkey)) { - fprintf(stderr, "Match error\n"); - goto done; - } - } - if (addent){ /* add entry */ - cxobj *xml = NULL; - - if (clicon_xml_parse(&xml, "%s", addstr) < 0) - goto done; - if (xmldb_put(h, db, OP_REPLACE, NULL, xml) < 0) - goto done; - if (xml) - xml_free(xml); - - } - if (rment) - if (remove_entry(db, rmkey) < 0) - goto done; - if (zapdb) /* remove databases */ - if (xmldb_delete(h, db) < 0){ - clicon_err(OE_FATAL, errno, "delete %s", db); - goto done; - } - if (initdb) - if (xmldb_init(h, db) < 0) - goto done; - done: - return 0; -} diff --git a/configure b/configure index f0d9de95..4f7dfcbd 100755 --- a/configure +++ b/configure @@ -1333,7 +1333,7 @@ Optional Packages: --with-cligen=dir Use CLIGEN here --without-restconf disable support for restconf --without-keyvalue disable support for key-value xmldb datastore - --with-qdbm=dir Use QDBM here + --with-qdbm=dir Use QDBM here, if keyvalue Some influential environment variables: CC C compiler command @@ -4266,7 +4266,7 @@ done -ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile datastore/text/Makefile doc/Makefile" +ac_config_files="$ac_config_files Makefile lib/Makefile lib/src/Makefile lib/clixon/Makefile apps/Makefile apps/cli/Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile include/Makefile etc/Makefile etc/clixonrc example/Makefile example/docker/Makefile docker/Makefile docker/cli/Makefile docker/cli/Dockerfile docker/backend/Makefile docker/backend/Dockerfile docker/netconf/Makefile docker/netconf/Dockerfile datastore/Makefile datastore/keyvalue/Makefile datastore/text/Makefile doc/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -4968,7 +4968,6 @@ do "apps/backend/Makefile") CONFIG_FILES="$CONFIG_FILES apps/backend/Makefile" ;; "apps/netconf/Makefile") CONFIG_FILES="$CONFIG_FILES apps/netconf/Makefile" ;; "apps/restconf/Makefile") CONFIG_FILES="$CONFIG_FILES apps/restconf/Makefile" ;; - "apps/dbctrl/Makefile") CONFIG_FILES="$CONFIG_FILES apps/dbctrl/Makefile" ;; "include/Makefile") CONFIG_FILES="$CONFIG_FILES include/Makefile" ;; "etc/Makefile") CONFIG_FILES="$CONFIG_FILES etc/Makefile" ;; "etc/clixonrc") CONFIG_FILES="$CONFIG_FILES etc/clixonrc" ;; diff --git a/configure.ac b/configure.ac index 9b12e28d..e5c562bf 100644 --- a/configure.ac +++ b/configure.ac @@ -183,7 +183,6 @@ AC_OUTPUT(Makefile apps/backend/Makefile apps/netconf/Makefile apps/restconf/Makefile - apps/dbctrl/Makefile include/Makefile etc/Makefile etc/clixonrc diff --git a/datastore/Makefile.in b/datastore/Makefile.in index 76e7a461..907321e3 100644 --- a/datastore/Makefile.in +++ b/datastore/Makefile.in @@ -31,8 +31,18 @@ # ***** END LICENSE BLOCK ***** # VPATH = @srcdir@ +prefix = @prefix@ +datarootdir = @datarootdir@ srcdir = @srcdir@ top_srcdir = @top_srcdir@ +exec_prefix = @exec_prefix@ +bindir = @bindir@ +libdir = @libdir@ +dbdir = @prefix@/db +mandir = @mandir@ +libexecdir = @libexecdir@ +localstatedir = @localstatedir@ +sysconfdir = @sysconfdir@ CC = @CC@ CFLAGS = @CFLAGS@ LDFLAGS = @LDFLAGS@ @@ -40,7 +50,22 @@ LIBS = @LIBS@ with_restconf = @with_restconf@ with_keyvalue = @with_keyvalue@ -SHELL = /bin/sh +SH_SUFFIX = @SH_SUFFIX@ +CLIXON_MAJOR = @CLIXON_VERSION_MAJOR@ +CLIXON_MINOR = @CLIXON_VERSION_MINOR@ + +# Use this clixon lib for linking +CLIXON_LIB = libclixon.so.$(CLIXON_MAJOR).$(CLIXON_MINOR) + +# For dependency +LIBDEPS = $(top_srcdir)/lib/src/$(CLIXON_LIB) + +LIBS = -L$(top_srcdir)/lib/src @LIBS@ -l:$(CLIXON_LIB) + +CPPFLAGS = @CPPFLAGS@ + +INCLUDES = -I. -I$(top_srcdir)/lib/src -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ + SUBDIRS = text ifeq ($(with_keyvalue),yes) @@ -49,7 +74,23 @@ endif .PHONY: all clean depend install $(SUBDIRS) -all: $(SUBDIRS) +APPSRC = datastore_client.c +APPOBJ = $(APPSRC:.c=.o) +APPL = datastore_client + +all: $(SUBDIRS) $(APPL) + +-include $(DESTDIR)$(datarootdir)/clixon/clixon.mk + +.SUFFIXES: +.SUFFIXES: .c .o + +.c.o: + $(CC) $(INCLUDES) -D__PROGRAM__=\"$(APPL)\" $(CPPFLAGS) $(CFLAGS) -c $< + +$(APPL) : $(APPOBJ) $(LIBDEPS) + $(CC) $(LDFLAGS) $(APPOBJ) $(LIBS) -o $@ + depend: for i in $(SUBDIRS); \ @@ -62,15 +103,19 @@ install-include: for i in $(SUBDIRS); \ do (cd $$i ; $(MAKE) $(MFLAGS) $@); done; -install: +install: $(APPL) + install -d $(DESTDIR)$(bindir) + install $(APPL) $(DESTDIR)$(bindir) for i in $(SUBDIRS); \ do (cd $$i; $(MAKE) $(MFLAGS) $@); done uninstall: + rm -f $(bindir)/$(APPL) for i in $(SUBDIRS); \ do (cd $$i; $(MAKE) $(MFLAGS) $@); done clean: + rm -f *.core $(APPL) $(APPOBJ) for i in $(SUBDIRS); \ do (cd $$i; $(MAKE) $(MFLAGS) $@); done diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c new file mode 100644 index 00000000..c43303f7 --- /dev/null +++ b/datastore/datastore_client.c @@ -0,0 +1,275 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +/* Command line options to be passed to getopt(3) */ +#define DATASTORE_OPTS "hDd:p:b:y:m:" + +/*! usage + */ +static void +usage(char *argv0) +{ + fprintf(stderr, "usage:%s * []\n" + "where options are\n" + "\t-h\t\tHelp\n" + "\t-D\t\tDebug\n" + "\t-d \t\tDatabase name. Default: running. Alt: candidate,startup\n" + "\t-b \tDatabase directory. Mandatory\n" + "\t-p \tDatastore plugin. Mandatory\n" + "\t-y \tYang directory (where modules are stored). Mandatory\n" + "\t-m \tYang module. Mandatory\n" + "and command is either:\n" + "\tget \n" + "\tput (set|merge|delete) \tXML on stdin\n" + "\tcopy \n" + "\tlock \n" + "\tunlock \n" + "\tunlock_all \n" + "\tislocked\n" + "\texists\n" + "\tdelete\n" + "\tinit\n" + , + argv0 + ); + exit(0); +} + +int +main(int argc, char **argv) +{ + char c; + clicon_handle h; + char *argv0; + char *db = "running"; + char *plugin = NULL; + char *cmd = NULL; + yang_spec *yspec = NULL; + char *yangdir = NULL; + char *yangmodule = NULL; + char *dbdir = NULL; + int ret; + int pid; + enum operation_type op; + cxobj *xt; + cxobj *xn; + + /* In the startup, logs to stderr & debug flag set later */ + clicon_log_init(__PROGRAM__, LOG_INFO, CLICON_LOG_STDERR); + + argv0 = argv[0]; + /* Defaults */ + if ((h = clicon_handle_init()) == NULL) + goto done; + /* getopt in two steps, first find config-file before over-riding options. */ + while ((c = getopt(argc, argv, DATASTORE_OPTS)) != -1) + switch (c) { + case '?' : + case 'h' : /* help */ + usage(argv0); + break; + case 'D' : /* debug */ + debug = 1; + break; + case 'd': /* db symbolic: running|candidate|startup */ + if (!optarg) + usage(argv0); + db = optarg; + break; + case 'p': /* datastore plugin */ + if (!optarg) + usage(argv0); + plugin = optarg; + break; + case 'b': /* db directory */ + if (!optarg) + usage(argv0); + dbdir = optarg; + break; + case 'y': /* Yang directory */ + if (!optarg) + usage(argv0); + yangdir = optarg; + break; + case 'm': /* Yang module */ + if (!optarg) + usage(argv0); + yangmodule = optarg; + break; + } + /* + * Logs, error and debug to stderr, set debug level + */ + clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, CLICON_LOG_STDERR); + clicon_debug_init(debug, NULL); + + argc -= optind; + argv += optind; + if (argc < 1) + usage(argv0); + cmd = argv[0]; + if (plugin == NULL){ + clicon_err(OE_DB, 0, "Missing plugin -p option"); + goto done; + } + if (dbdir == NULL){ + clicon_err(OE_DB, 0, "Missing dbdir -b option"); + goto done; + } + if (yangdir == NULL){ + clicon_err(OE_YANG, 0, "Missing yangdir -y option"); + goto done; + } + if (yangmodule == NULL){ + clicon_err(OE_YANG, 0, "Missing yang module -m option"); + goto done; + } + /* Load datastore plugin */ + if (xmldb_plugin_load(h, plugin) < 0) + goto done; + /* Connect to plugin to get a handle */ + if (xmldb_connect(h) < 0) + goto done; + /* Create yang spec */ + if ((yspec = yspec_new()) == NULL) + goto done; + /* Parse yang spec from given file */ + if (yang_parse(h, yangdir, yangmodule, NULL, yspec) < 0) + goto done; + /* Set database directory option */ + if (xmldb_setopt(h, "dbdir", dbdir) < 0) + goto done; + /* Set yang spec option */ + if (xmldb_setopt(h, "yangspec", yspec) < 0) + goto done; + if (strcmp(cmd, "get")==0){ + if (argc < 2) + usage(argv0); + if (xmldb_get(h, db, argv[1], &xt, NULL, 0) < 0) + goto done; + clicon_xml2file(stdout, xt, 0, 1); + } + else if (strcmp(cmd, "put")==0){ + if (argc < 3) + usage(argv0); + if (xml_operation(argv[1], &op) < 0) + usage(argv0); + if (clicon_xml_parse_file(0, &xt, "") < 0) + goto done; + if (xml_rootchild(xt, 0, &xn) < 0) + goto done; + if (xmldb_put(h, db, op, argv[2], xn) < 0) + goto done; + if (xt) + xml_free(xt); + } + else if (strcmp(cmd, "copy")==0){ + if (argc < 3) + usage(argv0); + if (xmldb_copy(h, argv[1], argv[2]) < 0) + goto done; + } + else if (strcmp(cmd, "lock")==0){ + if (argc < 2) + usage(argv0); + pid = atoi(argv[1]); + if (xmldb_lock(h, db, pid) < 0) + goto done; + } + else if (strcmp(cmd, "unlock")==0){ + if (argc < 2) + usage(argv0); + pid = atoi(argv[1]); + if (xmldb_unlock(h, db, pid) < 0) + goto done; + } + else if (strcmp(cmd, "unlock_all")==0){ + if (argc < 2) + usage(argv0); + pid = atoi(argv[1]); + if (xmldb_unlock_all(h, pid) < 0) + goto done; + } + else if (strcmp(cmd, "islocked")==0){ + if ((ret = xmldb_islocked(h, db)) < 0) + goto done; + fprintf(stdout, "islocked: %d\n", ret); + } + else if (strcmp(cmd, "exists")==0){ + if ((ret = xmldb_exists(h, db)) < 0) + goto done; + fprintf(stdout, "exists: %d\n", ret); + } + else if (strcmp(cmd, "delete")==0){ + if (xmldb_delete(h, db) < 0) + goto done; + } + else if (strcmp(cmd, "init")==0){ + if (xmldb_init(h, db) < 0) + goto done; + } + done: + return 0; +} + diff --git a/datastore/keyvalue/Makefile.in b/datastore/keyvalue/Makefile.in index 7db2ef56..9a116997 100644 --- a/datastore/keyvalue/Makefile.in +++ b/datastore/keyvalue/Makefile.in @@ -30,6 +30,7 @@ # # ***** END LICENSE BLOCK ***** # +VPATH = @srcdir@ prefix = @prefix@ datarootdir = @datarootdir@ srcdir = @srcdir@ diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 68e67a51..ab8565b3 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -81,6 +81,10 @@ * - restconf * - expand_dbvar * - show_conf_xpath + * + * dependency on clixon handle: + * clixon_xmldb_dir() + * clicon_dbspec_yang(h) */ #ifdef HAVE_CONFIG_H @@ -111,6 +115,31 @@ #include "clixon_qdb.h" #include "clixon_keyvalue.h" +#define handle(xh) (assert(kv_handle_check(xh)==0),(struct kv_handle *)(xh)) + +/* Magic to ensure plugin sanity. */ +#define KV_HANDLE_MAGIC 0xfa61a402 + +/*! Internal structure of keyvalue datastore handle. + */ +struct kv_handle { + int kh_magic; /* magic */ + char *kh_dbdir; /* Directory of database files */ + yang_spec *kh_yangspec; /* Yang spec if this datastore */ +}; + +/*! Check struct magic number for sanity checks + * return 0 if OK, -1 if fail. + */ +static int +kv_handle_check(xmldb_handle xh) +{ + /* Dont use handle macro to avoid recursion */ + struct kv_handle *kh = (struct kv_handle *)(xh); + + return kh->kh_magic == KV_HANDLE_MAGIC ? 0 : -1; +} + /*! Database locking for candidate and running non-persistent * Store an integer for running and candidate containing * the session-id of the client holding the lock. @@ -120,7 +149,7 @@ static int _candidate_locked = 0; static int _startup_locked = 0; /*! Translate from symbolic database name to actual filename in file-system - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Symbolic database name, eg "candidate", "running" * @param[out] filename Filename. Unallocate after use with free() * @retval 0 OK @@ -131,9 +160,9 @@ static int _startup_locked = 0; * The filename reside in CLICON_XMLDB_DIR option */ static int -db2file(clicon_handle h, - char *db, - char **filename) +db2file(struct kv_handle *kh, + char *db, + char **filename) { int retval = -1; cbuf *cb; @@ -143,8 +172,8 @@ db2file(clicon_handle h, clicon_err(OE_XML, errno, "cbuf_new"); goto done; } - if ((dir = clicon_xmldb_dir(h)) == NULL){ - clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set"); + if ((dir = kh->kh_dbdir) == NULL){ + clicon_err(OE_XML, errno, "dbdir not set"); goto done; } if (strcmp(db, "running") != 0 && @@ -244,49 +273,6 @@ create_keyvalues(cxobj *x, return retval; } -/*! Prune everything that has not been marked - * @param[in] xt XML tree with some node marked - * @param[out] upmark Set if a child (recursively) has marked set. - * The function removes all branches that does not contain a marked child - * XXX: maybe key leafs should not be purged if list is not purged? - * XXX: consider move to clicon_xml - */ -static int -xml_tree_prune_unmarked(cxobj *xt, - int *upmark) -{ - int retval = -1; - int submark; - int mark; - cxobj *x; - cxobj *xprev; - - mark = 0; - x = NULL; - xprev = x = NULL; - while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { - if (xml_flag(x, XML_FLAG_MARK)){ - mark++; - xprev = x; - continue; /* mark and stop here */ - } - if (xml_tree_prune_unmarked(x, &submark) < 0) - goto done; - if (submark) - mark++; - else{ /* Safe with xml_child_each if last */ - if (xml_purge(x) < 0) - goto done; - x = xprev; - } - xprev = x; - } - retval = 0; - done: - if (upmark) - *upmark = mark; - return retval; -} /*! * @param[in] xk xmlkey @@ -476,135 +462,116 @@ get(char *dbname, return retval; } -/*! Sanitize an xml tree: xml node has matching yang_stmt pointer +/*! Connect to a datastore plugin + * @retval handle Use this handle for other API calls + * @retval NULL Error + * @note You can do several connects, and have multiple connections to the same + * datastore */ -static int -xml_sanity(cxobj *x, - void *arg) +xmldb_handle +kv_connect(void) { - int retval = -1; - yang_stmt *ys; + struct kv_handle *kh; + xmldb_handle xh = NULL; + int size; - ys = (yang_stmt*)xml_spec(x); - if (ys==NULL){ - clicon_err(OE_XML, 0, "No spec for xml node %s", xml_name(x)); + size = sizeof(struct kv_handle); + if ((kh = malloc(size)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); goto done; } - if (strstr(ys->ys_argument, xml_name(x))==NULL){ - clicon_err(OE_XML, 0, "xml node name '%s' does not match yang spec arg '%s'", - xml_name(x), ys->ys_argument); - goto done; - } - retval = 0; - done: - return retval; + memset(kh, 0, size); + kh->kh_magic = KV_HANDLE_MAGIC; + xh = (xmldb_handle)kh; + done: + return xh; } -/*! Add default values (if not set) - */ -static int -xml_default(cxobj *x, - void *arg) +/*! Disconnect from a datastore plugin and deallocate handle + * @param[in] handle Disconect and deallocate from this handle + * @retval 0 OK + */ +int +kv_disconnect(xmldb_handle xh) { - int retval = -1; - yang_stmt *ys; - yang_stmt *y; - int i; - cxobj *xc; - cxobj *xb; - char *str; + int retval = -1; + struct kv_handle *kh = handle(xh); - ys = (yang_stmt*)xml_spec(x); - /* Check leaf defaults */ - if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){ - for (i=0; iys_len; i++){ - y = ys->ys_stmt[i]; - if (y->ys_keyword != Y_LEAF) - continue; - assert(y->ys_cv); - if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */ - if (!xml_find(x, y->ys_argument)){ - if ((xc = xml_new_spec(y->ys_argument, x, y)) == NULL) - goto done; - if ((xb = xml_new("body", xc)) == NULL) - goto done; - xml_type_set(xb, CX_BODY); - if ((str = cv2str_dup(y->ys_cv)) == NULL){ - clicon_err(OE_UNIX, errno, "cv2str_dup"); - goto done; - } - if (xml_value_set(xb, str) < 0) - goto done; - free(str); - } - } - } - } - retval = 0; - done: - return retval; -} - -/*! Order XML children according to YANG - */ -static int -xml_order(cxobj *x, - void *arg) -{ - int retval = -1; - yang_stmt *y; - yang_stmt *yc; - int i; - int j0; - int j; - cxobj *xc; - cxobj *xj; - char *yname; /* yang child name */ - char *xname; /* xml child name */ - - y = (yang_stmt*)xml_spec(x); - j0 = 0; - /* Go through xml children and ensure they are same order as yspec children */ - for (i=0; iys_len; i++){ - yc = y->ys_stmt[i]; - if (!yang_is_syntax(yc)) - continue; - yname = yc->ys_argument; - /* First go thru xml children with same name */ - for (;j0kh_dbdir) + free(kh->kh_dbdir); + free(kh); } retval = 0; // done: return retval; } +/*! Get value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[out] value Pointer to Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +kv_getopt(xmldb_handle xh, + char *optname, + void **value) +{ + int retval = -1; + struct kv_handle *kh = handle(xh); + + if (strcmp(optname, "yangspec") == 0) + *value = kh->kh_yangspec; + else if (strcmp(optname, "dbdir") == 0) + *value = kh->kh_dbdir; + else{ + clicon_err(OE_PLUGIN, 0, "Option %s not implemented by plugin", optname); + goto done; + } + retval = 0; + done: + return retval; +} + +/*! Set value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[in] value Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +kv_setopt(xmldb_handle xh, + char *optname, + void *value) +{ + int retval = -1; + struct kv_handle *kh = handle(xh); + + if (strcmp(optname, "yangspec") == 0) + kh->kh_yangspec = (yang_spec*)value; + else if (strcmp(optname, "dbdir") == 0){ + if (value && (kh->kh_dbdir = strdup((char*)value)) == NULL){ + clicon_err(OE_UNIX, 0, "strdup"); + goto done; + } + } + else{ + clicon_err(OE_PLUGIN, 0, "Option %s not implemented by plugin", optname); + goto done; + } + retval = 0; + done: + return retval; +} + /*! Get content of database using xpath. return a set of matching sub-trees * The function returns a minimal tree that includes all sub-trees that match * xpath. * @param[in] dbname Name of database to search in (filename including dir path * @param[in] xpath String with XPATH syntax. or NULL for all - * @param[in] yspec Yang specification * @param[out] xtop Single XML tree which xvec points to. Free with xml_free() * @param[out] xvec Vector of xml trees. Free after use. * @param[out] xlen Length of vector. @@ -614,8 +581,7 @@ xml_order(cxobj *x, * cxobj *xt; * cxobj **xvec; * size_t xlen; - * yang_spec *yspec = clicon_dbspec_yang(h); - * if (xmldb_get("running", "/interfaces/interface[name="eth"]", + * if (xmldb_get(xh, "running", "/interfaces/interface[name="eth"]", * &xt, &xvec, &xlen) < 0) * err; * for (i=0; ikh_yangspec) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } /* Read in complete database (this can be optimized) */ - if ((npairs = db_regexp(dbname, "", __FUNCTION__, &pairs, 0)) < 0) + if ((npairs = db_regexp(dbfile, "", __FUNCTION__, &pairs, 0)) < 0) goto done; if ((xt = xml_new_spec("clicon", NULL, yspec)) == NULL) goto done; /* Translate to complete xml tree */ for (i = 0; i < npairs; i++) { - if (get(dbname, + if (get(dbfile, yspec, pairs[i].dp_key, /* xml key */ pairs[i].dp_val, /* may be NULL */ @@ -693,7 +661,6 @@ kv_get(clicon_handle h, *xlen0 = xlen; xlen = 0; } - if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) goto done; /* XXX does not work for top-level */ @@ -706,8 +673,8 @@ kv_get(clicon_handle h, *xtop = xt; retval = 0; done: - if (dbname) - free(dbname); + if (dbfile) + free(dbfile); if (xvec) free(xvec); unchunk_group(__FUNCTION__); @@ -716,7 +683,7 @@ kv_get(clicon_handle h, } /*! Add data to database internal recursive function - * @param[in] dbname Name of database to search in (filename incl dir path) + * @param[in] dbfile Name of database to search in (filename incl dir path) * @param[in] xt xml-node. * @param[in] ys Yang statement corresponding to xml-node * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc @@ -726,7 +693,7 @@ kv_get(clicon_handle h, * @note XXX op only supports merge */ static int -put(char *dbname, +put(char *dbfile, cxobj *xt, yang_stmt *ys, enum operation_type op, @@ -774,7 +741,7 @@ put(char *dbname, /* Write to database, key and a vector of variables */ switch (op){ case OP_CREATE: - if ((exists = db_exists(dbname, xk)) < 0) + if ((exists = db_exists(dbfile, xk)) < 0) goto done; if (exists == 1){ clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk); @@ -782,18 +749,18 @@ put(char *dbname, } case OP_MERGE: case OP_REPLACE: - if (db_set(dbname, xk, body?body:NULL, body?strlen(body)+1:0) < 0) + if (db_set(dbfile, xk, body?body:NULL, body?strlen(body)+1:0) < 0) goto done; break; case OP_DELETE: - if ((exists = db_exists(dbname, xk)) < 0) + if ((exists = db_exists(dbfile, xk)) < 0) goto done; if (exists == 0){ clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); goto done; } case OP_REMOVE: - if (db_del(dbname, xk) < 0) + if (db_del(dbfile, xk) < 0) goto done; break; case OP_NONE: @@ -805,7 +772,7 @@ put(char *dbname, clicon_err(OE_UNIX, 0, "No yang node found: %s", xml_name(x)); goto done; } - if (put(dbname, x, y, op, xk) < 0) + if (put(dbfile, x, y, op, xk) < 0) goto done; } retval = 0; @@ -818,7 +785,7 @@ put(char *dbname, } /*! Modify database provided an XML database key and an operation - * @param[in] h CLICON handle + * @param[in] kh Keyvalue handle * @param[in] db Database name * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc * @param[in] xk XML Key, eg /aa/bb=17/name @@ -833,7 +800,7 @@ put(char *dbname, * @see xmldb_put with xml-tree, no path */ static int -xmldb_put_xkey(clicon_handle h, +xmldb_put_xkey(struct kv_handle *kh, char *db, enum operation_type op, char *xk, @@ -865,8 +832,11 @@ xmldb_put_xkey(clicon_handle h, yang_spec *yspec; char *filename = NULL; - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &filename) < 0) + if ((yspec = kh->kh_yangspec) == NULL){ + clicon_err(OE_YANG, ENOENT, "No yang spec"); + goto done; + } + if (db2file(kh, db, &filename) < 0) goto done; if (xk == NULL || *xk!='/'){ clicon_err(OE_DB, 0, "Invalid key: %s", xk); @@ -1045,7 +1015,7 @@ xmldb_put_xkey(clicon_handle h, /*! Modify database provided an xml tree, a restconf api_path and an operation * - * @param[in] h CLICON handle + * @param[in] kh Keyvalue handle * @param[in] db running or candidate * @param[in] op OP_MERGE: just add it. * OP_REPLACE: first delete whole database @@ -1063,7 +1033,7 @@ xmldb_put_xkey(clicon_handle h, * @see xmldb_put */ static int -xmldb_put_restconf_api_path(clicon_handle h, +xmldb_put_restconf_api_path(struct kv_handle *kh, char *db, enum operation_type op, char *api_path, @@ -1092,8 +1062,11 @@ xmldb_put_restconf_api_path(clicon_handle h, char *key; char *keys; - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &filename) < 0) + if ((yspec = kh->kh_yangspec) == NULL){ + clicon_err(OE_YANG, ENOENT, "No yang spec"); + goto done; + } + if (db2file(kh, db, &filename) < 0) goto done; if (api_path == NULL || *api_path!='/'){ clicon_err(OE_DB, 0, "Invalid api path: %s", api_path); @@ -1260,7 +1233,7 @@ xmldb_put_restconf_api_path(clicon_handle h, /*! Modify database provided an xml tree and an operation * - * @param[in] h CLICON handle + * @param[in] xh XMLDB handle * @param[in] db running or candidate * @param[in] xt xml-tree. Top-level symbol is dummy * @param[in] op OP_MERGE: just add it. @@ -1280,22 +1253,26 @@ xmldb_put_restconf_api_path(clicon_handle h, * @see xmldb_put_xkey for single key */ int -kv_put(clicon_handle h, +kv_put(xmldb_handle xh, char *db, enum operation_type op, char *api_path, cxobj *xt) { int retval = -1; + struct kv_handle *kh = handle(xh); cxobj *x = NULL; yang_stmt *ys; yang_spec *yspec; char *dbfilename = NULL; if (xml_child_nr(xt)==0 || xml_body(xt)!= NULL) - return xmldb_put_xkey(h, db, op, api_path, xml_body(xt)); - yspec = clicon_dbspec_yang(h); - if (db2file(h, db, &dbfilename) < 0) + return xmldb_put_xkey(kh, db, op, api_path, xml_body(xt)); + if ((yspec = kh->kh_yangspec) == NULL){ + clicon_err(OE_YANG, ENOENT, "No yang spec"); + goto done; + } + if (db2file(kh, db, &dbfilename) < 0) goto done; if (op == OP_REPLACE){ if (db_delete(dbfilename) < 0) @@ -1305,7 +1282,7 @@ kv_put(clicon_handle h, } while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ if (api_path && strlen(api_path)){ - if (xmldb_put_restconf_api_path(h, db, op, api_path, x) < 0) + if (xmldb_put_restconf_api_path(kh, db, op, api_path, x) < 0) goto done; continue; } @@ -1328,58 +1305,27 @@ kv_put(clicon_handle h, return retval; } -/*! Raw dump of database, just keys and values, no xml interpretation - * @param[in] f File - * @param[in] dbfile File-name of database. This is a local file - * @param[in] rxkey Key regexp, eg "^.*$" - * @note This function can only be called locally. - */ -int -kv_dump(FILE *f, - char *dbfilename, - char *rxkey) -{ - int retval = -1; - int npairs; - struct db_pair *pairs; - - /* Default is match all */ - if (rxkey == NULL) - rxkey = "^.*$"; - - /* Get all keys/values for vector */ - if ((npairs = db_regexp(dbfilename, rxkey, __FUNCTION__, &pairs, 0)) < 0) - goto done; - - for (npairs--; npairs >= 0; npairs--) - fprintf(f, "%s %s\n", pairs[npairs].dp_key, - pairs[npairs].dp_val?pairs[npairs].dp_val:""); - retval = 0; - done: - unchunk_group(__FUNCTION__); - return retval; -} - /*! Copy database from db1 to db2 - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] from Source database copy * @param[in] to Destination database * @retval -1 Error * @retval 0 OK */ int -kv_copy(clicon_handle h, +kv_copy(xmldb_handle xh, char *from, char *to) { int retval = -1; + struct kv_handle *kh = handle(xh); char *fromfile = NULL; char *tofile = NULL; /* XXX lock */ - if (db2file(h, from, &fromfile) < 0) + if (db2file(kh, from, &fromfile) < 0) goto done; - if (db2file(h, to, &tofile) < 0) + if (db2file(kh, to, &tofile) < 0) goto done; if (clicon_file_copy(fromfile, tofile) < 0) goto done; @@ -1393,17 +1339,19 @@ kv_copy(clicon_handle h, } /*! Lock database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @param[in] pid Process id * @retval -1 Error * @retval 0 OK */ int -kv_lock(clicon_handle h, +kv_lock(xmldb_handle xh, char *db, int pid) { + // struct kv_handle *kh = handle(xh); + if (strcmp("running", db) == 0) _running_locked = pid; else if (strcmp("candidate", db) == 0) @@ -1415,7 +1363,7 @@ kv_lock(clicon_handle h, } /*! Unlock database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @param[in] pid Process id * @retval -1 Error @@ -1423,10 +1371,12 @@ kv_lock(clicon_handle h, * Assume all sanity checks have been made */ int -kv_unlock(clicon_handle h, +kv_unlock(xmldb_handle xh, char *db, int pid) { + // struct kv_handle *kh = handle(xh); + if (strcmp("running", db) == 0) _running_locked = 0; else if (strcmp("candidate", db) == 0) @@ -1437,15 +1387,17 @@ kv_unlock(clicon_handle h, } /*! Unlock all databases locked by pid (eg process dies) - * @param[in] h Clicon handle - * @param[in] pid Process / Session id - * @retval -1 Error - * @retval 0 Ok + * @param[in] xh XMLDB handle + * @param[in] pid Process / Session id + * @retval -1 Error + * @retval 0 Ok */ int -kv_unlock_all(clicon_handle h, +kv_unlock_all(xmldb_handle xh, int pid) { + // struct kv_handle *kh = handle(xh); + if (_running_locked == pid) _running_locked = 0; if (_candidate_locked == pid) @@ -1456,16 +1408,18 @@ kv_unlock_all(clicon_handle h, } /*! Check if database is locked - * @param[in] h Clicon handle - * @param[in] db Database - * @retval -1 Error - * @retval 0 Not locked - * @retval >0 Id of locker + * @param[in] xh XMLDB handle + * @param[in] db Database + * @retval -1 Error + * @retval 0 Not locked + * @retval >0 Id of locker */ int -kv_islocked(clicon_handle h, +kv_islocked(xmldb_handle xh, char *db) { + // struct kv_handle *kh = handle(xh); + if (strcmp("running", db) == 0) return (_running_locked); else if (strcmp("candidate", db) == 0) @@ -1476,21 +1430,22 @@ kv_islocked(clicon_handle h, } /*! Check if db exists - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval -1 Error * @retval 0 No it does not exist * @retval 1 Yes it exists */ int -kv_exists(clicon_handle h, +kv_exists(xmldb_handle xh, char *db) { int retval = -1; + struct kv_handle *kh = handle(xh); char *filename = NULL; struct stat sb; - if (db2file(h, db, &filename) < 0) + if (db2file(kh, db, &filename) < 0) goto done; if (lstat(filename, &sb) < 0) retval = 0; @@ -1503,19 +1458,20 @@ kv_exists(clicon_handle h, } /*! Delete database. Remove file - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval -1 Error * @retval 0 OK */ int -kv_delete(clicon_handle h, +kv_delete(xmldb_handle xh, char *db) { int retval = -1; + struct kv_handle *kh = handle(xh); char *filename = NULL; - if (db2file(h, db, &filename) < 0) + if (db2file(kh, db, &filename) < 0) goto done; if (db_delete(filename) < 0) goto done; @@ -1527,19 +1483,20 @@ kv_delete(clicon_handle h, } /*! Initialize database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -kv_init(clicon_handle h, +kv_init(xmldb_handle xh, char *db) { int retval = -1; + struct kv_handle *kh = handle(xh); char *filename = NULL; - if (db2file(h, db, &filename) < 0) + if (db2file(kh, db, &filename) < 0) goto done; if (db_init(filename) < 0) goto done; @@ -1578,9 +1535,12 @@ static const struct xmldb_api api = { XMLDB_API_MAGIC, clixon_xmldb_plugin_init, kv_plugin_exit, + kv_connect, + kv_disconnect, + kv_getopt, + kv_setopt, kv_get, kv_put, - kv_dump, kv_copy, kv_lock, kv_unlock, @@ -1598,80 +1558,46 @@ static const struct xmldb_api api = { * Usage: clicon_xpath [] * read xml from input * Example compile: - gcc -g -o xmldb -I. -I../clixon ./clixon_xmldb.c -lclixon -lcligen + gcc -g -o keyvalue -I. -I../../lib ./clixon_keyvalue.c clixon_chunk.c clixon_qdb.c -lclixon -lcligen -lqdbm */ -static int -usage(char *argv0) -{ - fprintf(stderr, "usage:\n%s\tget []\t\txml on stdin\n", argv0); - fprintf(stderr, "\tput set|merge|delete\txml to stdout\n"); - exit(0); -} - +/*! Raw dump of database, just keys and values, no xml interpretation + * @param[in] f File + * @param[in] dbfile File-name of database. This is a local file + * @param[in] rxkey Key regexp, eg "^.*$" + * @note This function can only be called locally. + */ int -main(int argc, char **argv) +main(int argc, + char **argv) { - cxobj *xt; - cxobj *xn; - char *xpath; - enum operation_type op; - char *cmd; - char *db; - char *yangdir; - char *yangmod; - yang_spec *yspec = NULL; - clicon_handle h; + int retval = -1; + int npairs; + struct db_pair *pairs; + char *rxkey = NULL; + char *dbfilename; - if ((h = clicon_handle_init()) == NULL) - goto done; - clicon_log_init("xmldb", LOG_DEBUG, CLICON_LOG_STDERR); - if (argc < 4){ - usage(argv[0]); + if (argc != 2 && argc != 3){ + fprintf(stderr, "usage: %s [rxkey]\n", argv[0]); goto done; } - cmd = argv[1]; - db = argv[2]; - yangdir = argv[3]; - yangmod = argv[4]; - db_init(db); - if ((yspec = yspec_new()) == NULL) - goto done - if (yang_parse(h, yangdir, yangmod, NULL, yspec) < 0) - goto done; - if (strcmp(cmd, "get")==0){ - if (argc < 5) - usage(argv[0]); - xpath = argc>5?argv[5]:NULL; - if (xmldb_get(h, db, xpath, &xt, NULL, NULL) < 0) - goto done; - clicon_xml2file(stdout, xt, 0, 1); - } + dbfilename = argv[1]; + if (argc == 3) + rxkey = argv[2]; else - if (strcmp(cmd, "put")==0){ - if (argc != 6) - usage(argv[0]); - if (clicon_xml_parse_file(0, &xt, "") < 0) - goto done; - if (xml_rootchild(xt, 0, &xn) < 0) - goto done; - if (strcmp(argv[5], "set") == 0) - op = OP_REPLACE; - else - if (strcmp(argv[4], "merge") == 0) - op = OP_MERGE; - else if (strcmp(argv[5], "delete") == 0) - op = OP_REMOVE; - else - usage(argv[0]); - if (xmldb_put(h, db, op, NULL, xn) < 0) - goto done; - } - else - usage(argv[0]); - printf("\n"); + rxkey = "^.*$"; /* Default is match all */ + + /* Get all keys/values for vector */ + if ((npairs = db_regexp(dbfilename, rxkey, __FUNCTION__, &pairs, 0)) < 0) + goto done; + + for (npairs--; npairs >= 0; npairs--) + fprintf(stdout, "%s %s\n", pairs[npairs].dp_key, + pairs[npairs].dp_val?pairs[npairs].dp_val:""); + retval = 0; done: - return 0; + unchunk_group(__FUNCTION__); + return retval; } #endif /* Test program */ diff --git a/datastore/keyvalue/clixon_keyvalue.h b/datastore/keyvalue/clixon_keyvalue.h index ed5cea51..85e6d8ab 100644 --- a/datastore/keyvalue/clixon_keyvalue.h +++ b/datastore/keyvalue/clixon_keyvalue.h @@ -39,18 +39,18 @@ /* * Prototypes */ -int kv_get(clicon_handle h, char *db, char *xpath, +int kv_get(xmldb_handle h, char *db, char *xpath, cxobj **xtop, cxobj ***xvec, size_t *xlen); -int kv_put(clicon_handle h, char *db, enum operation_type op, +int kv_put(xmldb_handle h, char *db, enum operation_type op, char *api_path, cxobj *xt); int kv_dump(FILE *f, char *dbfilename, char *rxkey); -int kv_copy(clicon_handle h, char *from, char *to); -int kv_lock(clicon_handle h, char *db, int pid); -int kv_unlock(clicon_handle h, char *db, int pid); -int kv_unlock_all(clicon_handle h, int pid); -int kv_islocked(clicon_handle h, char *db); -int kv_exists(clicon_handle h, char *db); -int kv_delete(clicon_handle h, char *db); -int kv_init(clicon_handle h, char *db); +int kv_copy(xmldb_handle h, char *from, char *to); +int kv_lock(xmldb_handle h, char *db, int pid); +int kv_unlock(xmldb_handle h, char *db, int pid); +int kv_unlock_all(xmldb_handle h, int pid); +int kv_islocked(xmldb_handle h, char *db); +int kv_exists(xmldb_handle h, char *db); +int kv_delete(xmldb_handle h, char *db); +int kv_init(xmldb_handle h, char *db); #endif /* _CLIXON_KEYVALUE_H */ diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 2b4926eb..f9c67fd7 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -44,11 +44,13 @@ #include #include #include +#include #include #include #include #include -#include +#include +#include /* cligen */ #include @@ -58,16 +60,42 @@ #include "clixon_xmldb_text.h" +#define handle(xh) (assert(text_handle_check(xh)==0),(struct text_handle *)(xh)) + +/* Magic to ensure plugin sanity. */ +#define TEXT_HANDLE_MAGIC 0x7f54da29 + +/*! Internal structure of text datastore handle. + */ +struct text_handle { + int th_magic; /* magic */ + char *th_dbdir; /* Directory of database files */ + yang_spec *th_yangspec; /* Yang spec if this datastore */ +}; + +/*! Check struct magic number for sanity checks + * return 0 if OK, -1 if fail. + */ +static int +text_handle_check(xmldb_handle xh) +{ + /* Dont use handle macro to avoid recursion */ + struct text_handle *th = (struct text_handle *)(xh); + + return th->th_magic == TEXT_HANDLE_MAGIC ? 0 : -1; +} + /*! Database locking for candidate and running non-persistent * Store an integer for running and candidate containing * the session-id of the client holding the lock. + * @note This should probably be on file-system */ static int _running_locked = 0; static int _candidate_locked = 0; static int _startup_locked = 0; /*! Translate from symbolic database name to actual filename in file-system - * @param[in] h Clicon handle + * @param[in] th text handle handle * @param[in] db Symbolic database name, eg "candidate", "running" * @param[out] filename Filename. Unallocate after use with free() * @retval 0 OK @@ -78,9 +106,9 @@ static int _startup_locked = 0; * The filename reside in CLICON_XMLDB_DIR option */ static int -db2file(clicon_handle h, - char *db, - char **filename) +db2file(struct text_handle *th, + char *db, + char **filename) { int retval = -1; cbuf *cb; @@ -90,8 +118,8 @@ db2file(clicon_handle h, clicon_err(OE_XML, errno, "cbuf_new"); goto done; } - if ((dir = clicon_xmldb_dir(h)) == NULL){ - clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set"); + if ((dir = th->th_dbdir) == NULL){ + clicon_err(OE_XML, errno, "dbdir not set"); goto done; } if (strcmp(db, "running") != 0 && @@ -113,12 +141,116 @@ db2file(clicon_handle h, return retval; } +/*! Connect to a datastore plugin + * @retval handle Use this handle for other API calls + * @retval NULL Error + */ +xmldb_handle +text_connect(void) +{ + struct text_handle *th; + xmldb_handle xh = NULL; + int size; + + size = sizeof(struct text_handle); + if ((th = malloc(size)) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(th, 0, size); + th->th_magic = TEXT_HANDLE_MAGIC; + xh = (xmldb_handle)th; + done: + return xh; + +} + +/*! Disconnect from to a datastore plugin and deallocate handle + * @param[in] xh XMLDB handle, disconect and deallocate from this handle + * @retval 0 OK + */ +int +text_disconnect(xmldb_handle xh) +{ + int retval = -1; + struct text_handle *th = handle(xh); + + if (th){ + if (th->th_dbdir) + free(th->th_dbdir); + free(th); + } + retval = 0; + // done: + return retval; +} + +/*! Get value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[out] value Pointer to Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +text_getopt(xmldb_handle xh, + char *optname, + void **value) +{ + int retval = -1; + struct text_handle *th = handle(xh); + + if (strcmp(optname, "yangspec") == 0) + *value = th->th_yangspec; + else if (strcmp(optname, "dbdir") == 0) + *value = th->th_dbdir; + else{ + clicon_err(OE_PLUGIN, 0, "Option %s not implemented by plugin", optname); + goto done; + } + retval = 0; + done: + return retval; +} + +/*! Set value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[in] value Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +text_setopt(xmldb_handle xh, + char *optname, + void *value) +{ + int retval = -1; + struct text_handle *th = handle(xh); + + if (strcmp(optname, "yangspec") == 0) + th->th_yangspec = (yang_spec*)value; + else if (strcmp(optname, "dbdir") == 0){ + if (value && (th->th_dbdir = strdup((char*)value)) == NULL){ + clicon_err(OE_UNIX, 0, "strdup"); + goto done; + } + } + else{ + clicon_err(OE_PLUGIN, 0, "Option %s not implemented by plugin", optname); + goto done; + } + retval = 0; + done: + return retval; +} + /*! Get content of database using xpath. return a set of matching sub-trees * The function returns a minimal tree that includes all sub-trees that match * xpath. + * @param[in] xh XMLDB handle * @param[in] dbname Name of database to search in (filename including dir path * @param[in] xpath String with XPATH syntax. or NULL for all - * @param[in] yspec Yang specification * @param[out] xtop Single XML tree which xvec points to. Free with xml_free() * @param[out] xvec Vector of xml trees. Free after use. * @param[out] xlen Length of vector. @@ -128,8 +260,7 @@ db2file(clicon_handle h, * cxobj *xt; * cxobj **xvec; * size_t xlen; - * yang_spec *yspec = clicon_dbspec_yang(h); - * if (xmldb_get("running", "/interfaces/interface[name="eth"]", + * if (xmldb_get(xh, "running", "/interfaces/interface[name="eth"]", * &xt, &xvec, &xlen) < 0) * err; * for (i=0; ith_yangspec) == NULL){ + clicon_err(OE_YANG, ENOENT, "No yang spec"); + goto done; + } + if ((fd = open(dbfile, O_RDONLY)) < 0){ + clicon_err(OE_UNIX, errno, "open(%s)", dbfile); + goto done; + } + if ((clicon_xml_parse_file(fd, &xt, "")) < 0) + goto done; + /* XXX Maybe the below is general function and should be moved to xmldb? */ + + if (xpath_vec(xt, xpath?xpath:"/", &xvec, &xlen) < 0) + goto done; + + /* If vectors are specified then filter out everything else, + * otherwise return complete tree. + */ + if (xvec != NULL){ + for (i=0; i1) + clicon_xml2file(stderr, xt, 0, 1); + *xtop = xt; retval = 0; - // done: + done: + // xml_free(xt); + if (fd != -1) + close(fd); return retval; } - /*! Modify database provided an xml tree and an operation * - * @param[in] h CLICON handle + * @param[in] xh XMLDB handle * @param[in] db running or candidate * @param[in] xt xml-tree. Top-level symbol is dummy * @param[in] op OP_MERGE: just add it. @@ -182,30 +373,14 @@ text_get(clicon_handle h, * @see xmldb_put_xkey for single key */ int -text_put(clicon_handle h, +text_put(xmldb_handle xh, char *db, enum operation_type op, char *api_path, cxobj *xt) { - int retval = -1; - retval = 0; - // done: - return retval; -} - -/*! Raw dump of database, just keys and values, no xml interpretation - * @param[in] f File - * @param[in] dbfile File-name of database. This is a local file - * @param[in] rxkey Key regexp, eg "^.*$" - * @note This function can only be called locally. - */ -int -text_dump(FILE *f, - char *dbfilename, - char *rxkey) -{ - int retval = -1; + int retval = -1; + // struct text_handle *th = handle(xh); retval = 0; // done: @@ -213,35 +388,39 @@ text_dump(FILE *f, } /*! Copy database from db1 to db2 - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] from Source database copy * @param[in] to Destination database * @retval -1 Error * @retval 0 OK */ int -text_copy(clicon_handle h, - char *from, - char *to) +text_copy(xmldb_handle xh, + char *from, + char *to) { - int retval = -1; + int retval = -1; + // struct text_handle *th = handle(xh); + retval = 0; // done: return retval; } /*! Lock database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @param[in] pid Process id * @retval -1 Error * @retval 0 OK */ int -text_lock(clicon_handle h, - char *db, - int pid) +text_lock(xmldb_handle xh, + char *db, + int pid) { + // struct text_handle *th = handle(xh); + if (strcmp("running", db) == 0) _running_locked = pid; else if (strcmp("candidate", db) == 0) @@ -253,7 +432,7 @@ text_lock(clicon_handle h, } /*! Unlock database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @param[in] pid Process id * @retval -1 Error @@ -261,10 +440,12 @@ text_lock(clicon_handle h, * Assume all sanity checks have been made */ int -text_unlock(clicon_handle h, - char *db, - int pid) +text_unlock(xmldb_handle xh, + char *db, + int pid) { + // struct text_handle *th = handle(xh); + if (strcmp("running", db) == 0) _running_locked = 0; else if (strcmp("candidate", db) == 0) @@ -275,15 +456,17 @@ text_unlock(clicon_handle h, } /*! Unlock all databases locked by pid (eg process dies) - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] pid Process / Session id * @retval -1 Error * @retval 0 Ok */ int -text_unlock_all(clicon_handle h, - int pid) +text_unlock_all(xmldb_handle xh, + int pid) { + // struct text_handle *th = handle(xh); + if (_running_locked == pid) _running_locked = 0; if (_candidate_locked == pid) @@ -294,16 +477,18 @@ text_unlock_all(clicon_handle h, } /*! Check if database is locked - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval -1 Error * @retval 0 Not locked * @retval >0 Id of locker */ int -text_islocked(clicon_handle h, - char *db) +text_islocked(xmldb_handle xh, + char *db) { + // struct text_handle *th = handle(xh); + if (strcmp("running", db) == 0) return (_running_locked); else if (strcmp("candidate", db) == 0) @@ -314,21 +499,23 @@ text_islocked(clicon_handle h, } /*! Check if db exists - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval -1 Error * @retval 0 No it does not exist * @retval 1 Yes it exists */ int -text_exists(clicon_handle h, - char *db) +text_exists(xmldb_handle xh, + char *db) { - int retval = -1; - char *filename = NULL; - struct stat sb; - if (db2file(h, db, &filename) < 0) + int retval = -1; + struct text_handle *th = handle(xh); + char *filename = NULL; + struct stat sb; + + if (db2file(th, db, &filename) < 0) goto done; if (lstat(filename, &sb) < 0) retval = 0; @@ -341,16 +528,17 @@ text_exists(clicon_handle h, } /*! Delete database. Remove file - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval -1 Error * @retval 0 OK */ int -text_delete(clicon_handle h, - char *db) +text_delete(xmldb_handle xh, + char *db) { - int retval = -1; + int retval = -1; +// struct text_handle *th = handle(xh); retval = 0; // done: @@ -358,16 +546,17 @@ text_delete(clicon_handle h, } /*! Initialize database - * @param[in] h Clicon handle + * @param[in] xh XMLDB handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -text_init(clicon_handle h, +text_init(xmldb_handle xh, char *db) { int retval = -1; + // struct text_handle *th = handle(xh); retval = 0; // done: @@ -402,9 +591,12 @@ static const struct xmldb_api api = { XMLDB_API_MAGIC, clixon_xmldb_plugin_init, text_plugin_exit, + text_connect, + text_disconnect, + text_getopt, + text_setopt, text_get, text_put, - text_dump, text_copy, text_lock, text_unlock, diff --git a/datastore/text/clixon_xmldb_text.h b/datastore/text/clixon_xmldb_text.h index b2ca183d..83f5db43 100644 --- a/datastore/text/clixon_xmldb_text.h +++ b/datastore/text/clixon_xmldb_text.h @@ -39,18 +39,18 @@ /* * Prototypes */ -int text_get(clicon_handle h, char *db, char *xpath, +int text_get(xmldb_handle h, char *db, char *xpath, cxobj **xtop, cxobj ***xvec, size_t *xlen); -int text_put(clicon_handle h, char *db, enum operation_type op, +int text_put(xmldb_handle h, char *db, enum operation_type op, char *api_path, cxobj *xt); int text_dump(FILE *f, char *dbfilename, char *rxkey); -int text_copy(clicon_handle h, char *from, char *to); -int text_lock(clicon_handle h, char *db, int pid); -int text_unlock(clicon_handle h, char *db, int pid); -int text_unlock_all(clicon_handle h, int pid); -int text_islocked(clicon_handle h, char *db); -int text_exists(clicon_handle h, char *db); -int text_delete(clicon_handle h, char *db); -int text_init(clicon_handle h, char *db); +int text_copy(xmldb_handle h, char *from, char *to); +int text_lock(xmldb_handle h, char *db, int pid); +int text_unlock(xmldb_handle h, char *db, int pid); +int text_unlock_all(xmldb_handle h, int pid); +int text_islocked(xmldb_handle h, char *db); +int text_exists(xmldb_handle h, char *db); +int text_delete(xmldb_handle h, char *db); +int text_init(xmldb_handle h, char *db); #endif /* _CLIXON_XMLDB_TEXT_H */ diff --git a/lib/clixon/clixon_handle.h b/lib/clixon/clixon_handle.h index 207d076f..c4f601b6 100644 --- a/lib/clixon/clixon_handle.h +++ b/lib/clixon/clixon_handle.h @@ -71,4 +71,10 @@ clicon_hash_t *clicon_options(clicon_handle h); /* Return internal clicon data (hash-array) given a handle.*/ clicon_hash_t *clicon_data(clicon_handle h); +/* Set or reset XMLDB storage handle */ +int clicon_handle_xmldb_set(clicon_handle h, void *xh); /* ie xmldb_handle */ + +/* Get XMLDB storage handle */ +void *clicon_handle_xmldb_get(clicon_handle h); + #endif /* _CLIXON_HANDLE_H_ */ diff --git a/lib/clixon/clixon_xml_db.h b/lib/clixon/clixon_xml_db.h index 04325360..3cad3679 100644 --- a/lib/clixon/clixon_xml_db.h +++ b/lib/clixon/clixon_xml_db.h @@ -34,6 +34,19 @@ #ifndef _CLIXON_XML_DB_H #define _CLIXON_XML_DB_H +/* The XMLDB has a handle to keep connected state. To users of the API it is + * a void* but it may have structure within a specific plugin. + * The handle is independent from clicon so that (in principle) the datastore + * can work in other contexts than clicon. + * The connect API call sets the handle and disconnect resets it. In principle + * there can be several handles at one time. + */ +#if 1 /* SANITY CHECK */ +typedef struct {int16_t a;} *xmldb_handle; +#else +typedef void *xmldb_handle; +#endif + /* Version of clixon datastore plugin API. */ #define XMLDB_API_VERSION 1 @@ -49,40 +62,49 @@ typedef void * (plugin_init_t)(int version); /* Type of plugin exit function */ typedef int (plugin_exit_t)(void); +/* Type of xmldb connect function */ +typedef xmldb_handle (xmldb_connect_t)(void); + +/* Type of xmldb disconnect function */ +typedef int (xmldb_disconnect_t)(xmldb_handle xh); + +/* Type of xmldb getopt function */ +typedef int (xmldb_getopt_t)(xmldb_handle xh, char *optname, void **value); + +/* Type of xmldb setopt function */ +typedef int (xmldb_setopt_t)(xmldb_handle xh, char *optname, void *value); + /* Type of xmldb get function */ -typedef int (xmldb_get_t)(clicon_handle h, char *db, char *xpath, +typedef int (xmldb_get_t)(xmldb_handle xh, char *db, char *xpath, cxobj **xtop, cxobj ***xvec, size_t *xlen); /* Type of xmldb put function */ -typedef int (xmldb_put_t)(clicon_handle h, char *db, enum operation_type op, - char *api_path, cxobj *xt); - -/* Type of xmldb dump function */ -typedef int (xmldb_dump_t)(FILE *f, char *dbfilename, char *rxkey); +typedef int (xmldb_put_t)(xmldb_handle xh, char *db, enum operation_type op, + char *api_path, cxobj *xt); /* Type of xmldb copy function */ -typedef int (xmldb_copy_t)(clicon_handle h, char *from, char *to); +typedef int (xmldb_copy_t)(xmldb_handle xh, char *from, char *to); /* Type of xmldb lock function */ -typedef int (xmldb_lock_t)(clicon_handle h, char *db, int pid); +typedef int (xmldb_lock_t)(xmldb_handle xh, char *db, int pid); /* Type of xmldb unlock function */ -typedef int (xmldb_unlock_t)(clicon_handle h, char *db, int pid); +typedef int (xmldb_unlock_t)(xmldb_handle xh, char *db, int pid); /* Type of xmldb unlock_all function */ -typedef int (xmldb_unlock_all_t)(clicon_handle h, int pid); +typedef int (xmldb_unlock_all_t)(xmldb_handle xh, int pid); /* Type of xmldb islocked function */ -typedef int (xmldb_islocked_t)(clicon_handle h, char *db); +typedef int (xmldb_islocked_t)(xmldb_handle xh, char *db); /* Type of xmldb exists function */ -typedef int (xmldb_exists_t)(clicon_handle h, char *db); +typedef int (xmldb_exists_t)(xmldb_handle xh, char *db); /* Type of xmldb delete function */ -typedef int (xmldb_delete_t)(clicon_handle h, char *db); +typedef int (xmldb_delete_t)(xmldb_handle xh, char *db); /* Type of xmldb init function */ -typedef int (xmldb_init_t)(clicon_handle h, char *db); +typedef int (xmldb_init_t)(xmldb_handle xh, char *db); /* grideye agent plugin init struct for the api */ struct xmldb_api{ @@ -90,9 +112,12 @@ struct xmldb_api{ int xa_magic; plugin_init_t *xa_plugin_init_fn; /* XMLDB_PLUGIN_INIT_FN */ plugin_exit_t *xa_plugin_exit_fn; + xmldb_connect_t *xa_connect_fn; + xmldb_disconnect_t *xa_disconnect_fn; + xmldb_getopt_t *xa_getopt_fn; + xmldb_setopt_t *xa_setopt_fn; xmldb_get_t *xa_get_fn; xmldb_put_t *xa_put_fn; - xmldb_dump_t *xa_dump_fn; xmldb_copy_t *xa_copy_fn; xmldb_lock_t *xa_lock_fn; xmldb_unlock_t *xa_unlock_fn; @@ -105,14 +130,19 @@ struct xmldb_api{ /* * Prototypes + * API */ -int xmldb_plugin_load(char *filename); +int xmldb_plugin_load(clicon_handle h, char *filename); +int xmldb_plugin_unload(clicon_handle h); +int xmldb_connect(clicon_handle h); +int xmldb_disconnect(clicon_handle h); +int xmldb_getopt(clicon_handle h, char *optname, void **value); +int xmldb_setopt(clicon_handle h, char *optname, void *value); int xmldb_get(clicon_handle h, char *db, char *xpath, cxobj **xtop, cxobj ***xvec, size_t *xlen); int xmldb_put(clicon_handle h, char *db, enum operation_type op, char *api_path, cxobj *xt); -int xmldb_dump(FILE *f, char *dbfilename, char *rxkey); int xmldb_copy(clicon_handle h, char *from, char *to); int xmldb_lock(clicon_handle h, char *db, int pid); int xmldb_unlock(clicon_handle h, char *db, int pid); diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 7cbc4680..dcc5f0ee 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -64,5 +64,9 @@ int xml_diff(yang_spec *yspec, cxobj *xt1, cxobj *xt2, int yang2xmlkeyfmt(yang_stmt *ys, int inclkey, char **xkfmt); int xmlkeyfmt2key(char *xkfmt, cvec *cvv, char **xk); int xmlkeyfmt2xpath(char *xkfmt, cvec *cvv, char **xk); +int xml_tree_prune_unmarked(cxobj *xt, int *upmark); +int xml_default(cxobj *x, void *arg); +int xml_order(cxobj *x, void *arg); +int xml_sanity(cxobj *x, void *arg); #endif /* _CLIXON_XML_MAP_H_ */ diff --git a/lib/src/clixon_handle.c b/lib/src/clixon_handle.c index ade4350d..47accd83 100644 --- a/lib/src/clixon_handle.c +++ b/lib/src/clixon_handle.c @@ -58,18 +58,17 @@ #define handle(h) (assert(clicon_handle_check(h)==0),(struct clicon_handle *)(h)) -/* - * clicon_handle - * Internal structire of basic handle. Also header of all other handles. - * see struct clicon_cli_handle, struct clicon_backend_handle, etc +/*! Internal structure of basic handle. Also header of all other handles. + * @note If you change here, you must also change the structs below: + * @see struct cli_handle, struct backend_handle */ struct clicon_handle { int ch_magic; /* magic (HDR) */ clicon_hash_t *ch_copt; /* clicon option list (HDR) */ clicon_hash_t *ch_data; /* internal clicon data (HDR) */ + void *ch_xmldb; /* XMLDB storage handle, uie xmldb_handle */ }; - /*! Internal call to allocate a CLICON handle. * * There may be different variants of handles with some common options. @@ -166,3 +165,31 @@ clicon_data(clicon_handle h) return ch->ch_data; } + +/*! Set or reset XMLDB storage handle + * @param[in] h Clicon handle + * @param[in] xh XMLDB storage handle. If NULL reset it + * @note Just keep note of it, dont allocate it or so. + */ +int +clicon_handle_xmldb_set(clicon_handle h, + void *xh) +{ + struct clicon_handle *ch = handle(h); + + ch->ch_xmldb = xh; + return 0; +} + +/*! Get XMLDB storage handle + * @param[in] h Clicon handle + * @retval xh XMLDB storage handle. If not connected return NULL + */ +void * +clicon_handle_xmldb_get(clicon_handle h) + +{ + struct clicon_handle *ch = handle(h); + + return ch->ch_xmldb; +} diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index bfcbcd61..22a575a3 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -1008,6 +1008,7 @@ FSM(char *tag, * Note, xt will add a top-level symbol called "top" meaning that will look as: * * XXX: There is a potential leak here on some return values. + * XXX: What happens if endtag is different? * May block */ int @@ -1023,15 +1024,16 @@ clicon_xml_parse_file(int fd, int maxbuf = BUFLEN; int endtaglen = strlen(endtag); int state = 0; + int oldmaxbuf; if (endtag == NULL){ clicon_err(OE_XML, 0, "%s: endtag required\n", __FUNCTION__); - return -1; + goto done; } *cx = NULL; if ((xmlbuf = malloc(maxbuf)) == NULL){ clicon_err(OE_XML, errno, "%s: malloc", __FUNCTION__); - return -1; + goto done; } memset(xmlbuf, 0, maxbuf); ptr = xmlbuf; @@ -1050,24 +1052,33 @@ clicon_xml_parse_file(int fd, state = 0; if ((*cx = xml_new("top", NULL)) == NULL) break; - if (xml_parse(ptr, *cx) < 0) + if (xml_parse(ptr, *cx) < 0){ + goto done; return -1; + } break; } if (len>=maxbuf-1){ /* Space: one for the null character */ - int oldmaxbuf = maxbuf; - + oldmaxbuf = maxbuf; maxbuf *= 2; if ((xmlbuf = realloc(xmlbuf, maxbuf)) == NULL){ clicon_err(OE_XML, errno, "%s: realloc", __FUNCTION__); - return -1; + goto done; } memset(xmlbuf+oldmaxbuf, 0, maxbuf-oldmaxbuf); ptr = xmlbuf; } } /* while */ - free(xmlbuf); - return (*cx)?0:-1; + retval = 0; + done: + if (retval < 0 && *cx){ + free(*cx); + *cx = NULL; + } + if (xmlbuf) + free(xmlbuf); + return retval; + // return (*cx)?0:-1; } /*! Read an XML definition from string and parse it into a parse-tree. @@ -1461,8 +1472,12 @@ xml_body_uint32(cxobj *xb, } /*! Map xml operation from string to enumeration - * @param[in] xn XML node - * @param[out] op "operation" attribute may change operation + * @param[in] opstr String, eg "merge" + * @param[out] op Enumeration, eg OP_MERGE + * @code + * enum operation_type op; + * xml_operation("replace", &op) + * @endcode */ int xml_operation(char *opstr, @@ -1487,6 +1502,14 @@ xml_operation(char *opstr, return 0; } +/*! Map xml operation from enumeration to string + * @param[in] op enumeration operation, eg OP_MERGE,... + * @retval str String, eg "merge". Static string, no free necessary + * @code + * enum operation_type op; + * xml_operation("replace", &op) + * @endcode + */ char * xml_operation2str(enum operation_type op) { @@ -1510,3 +1533,4 @@ xml_operation2str(enum operation_type op) return "none"; } } + diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 62d724f3..97ef6dd3 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -58,6 +58,8 @@ #include "clixon_hash.h" #include "clixon_handle.h" #include "clixon_xml.h" +#include "clixon_yang.h" +#include "clixon_options.h" #include "clixon_xml_db.h" static struct xmldb_api *_xa_api = NULL; @@ -65,12 +67,13 @@ static struct xmldb_api *_xa_api = NULL; /*! Load a specific plugin, call its init function and add it to plugins list * If init function fails (not found, wrong version, etc) print a log and dont * add it. - * @param[in] name Name of plugin + * @param[in] name Filename (complete path) of plugin * @param[in] filename Actual filename with path * @param[out] plugin Plugin data structure for invoking. Dealloc with free */ int -xmldb_plugin_load(char *filename) +xmldb_plugin_load(clicon_handle h, + char *filename) { int retval = -1; char *dlerrcode; @@ -117,11 +120,179 @@ xmldb_plugin_load(char *filename) goto done; } -int -xmldb_get(clicon_handle h, char *db, char *xpath, - cxobj **xtop, cxobj ***xvec, size_t *xlen) +/*! XXX: fixme */ +int +xmldb_plugin_unload(clicon_handle h) { - int retval = -1; + return 0; +} + +/*! Connect to a datastore plugin + * @retval handle Use this handle for other API calls + * @retval NULL Error + * @note You can do several connects, and have multiple connections to the same + * datastore. Note also that the xmldb handle is hidden in the clicon + * handle, the clixon user does not need to handle it. Note also that + * typically only the backend invokes the datastore. + * XXX what args does connect have? + */ +int +xmldb_connect(clicon_handle h) +{ + int retval = -1; + xmldb_handle xh; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_connect_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); + goto done; + } + if ((xh = _xa_api->xa_connect_fn()) == NULL) + goto done; + clicon_handle_xmldb_set(h, xh); + + retval = 0; + done: + return retval; +} + +/*! Disconnect from a datastore plugin and deallocate handle + * @param[in] handle Disconect and deallocate from this handle + * @retval 0 OK + */ +int +xmldb_disconnect(clicon_handle h) +{ + int retval = -1; + xmldb_handle xh; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_disconnect_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); + goto done; + } + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Already disconnected from datastore plugin"); + goto done; + } + if (_xa_api->xa_disconnect_fn(xh) < 0) + goto done; + clicon_handle_xmldb_set(h, NULL); + retval = 0; + done: + return retval; +} + +/*! Get value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[out] value Pointer to Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +xmldb_getopt(clicon_handle h, + char *optname, + void **value) +{ + int retval = -1; + xmldb_handle xh; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_getopt_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); + goto done; + } + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_getopt_fn(xh, optname, value); + done: + return retval; +} + +/*! Set value of generic plugin option. Type of value is givenby context + * @param[in] xh XMLDB handle + * @param[in] optname Option name + * @param[in] value Value of option + * @retval 0 OK + * @retval -1 Error + */ +int +xmldb_setopt(clicon_handle h, + char *optname, + void *value) +{ + int retval = -1; + xmldb_handle xh; + + if (_xa_api == NULL){ + clicon_err(OE_DB, 0, "No xmldb plugin"); + goto done; + } + if (_xa_api->xa_setopt_fn == NULL){ + clicon_err(OE_DB, 0, "No xmldb function"); + goto done; + } + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_setopt_fn(xh, optname, value); + done: + return retval; +} + +/*! Get content of database using xpath. return a set of matching sub-trees + * The function returns a minimal tree that includes all sub-trees that match + * xpath. + * @param[in] h Clicon handle + * @param[in] dbname Name of database to search in (filename including dir path + * @param[in] xpath String with XPATH syntax. or NULL for all + * @param[out] xtop Single XML tree which xvec points to. Free with xml_free() + * @param[out] xvec Vector of xml trees. Free after use. + * @param[out] xlen Length of vector. + * @retval 0 OK + * @retval -1 Error + * @code + * cxobj *xt; + * cxobj **xvec; + * size_t xlen; + * if (xmldb_get(xh, "running", "/interfaces/interface[name="eth"]", + * &xt, &xvec, &xlen) < 0) + * err; + * for (i=0; ixa_get_fn(h, db, xpath, xtop, xvec, xlen); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_get_fn(xh, db, xpath, xtop, xvec, xlen); done: return retval; } @@ -152,16 +327,20 @@ xmldb_get(clicon_handle h, char *db, char *xpath, * cxobj *xt; * if (clicon_xml_parse_str("17", &xt) < 0) * err; - * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) + * if (xmldb_put(xh, "running", OP_MERGE, NULL, xt) < 0) * err; * @endcode * @see xmldb_put_xkey for single key */ int -xmldb_put(clicon_handle h, char *db, enum operation_type op, - char *api_path, cxobj *xt) +xmldb_put(clicon_handle h, + char *db, + enum operation_type op, + char *api_path, + cxobj *xt) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -171,32 +350,11 @@ xmldb_put(clicon_handle h, char *db, enum operation_type op, clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_put_fn(h, db, op, api_path, xt); - done: - return retval; -} - -/*! Raw dump of database, in internal format (depends on datastore) - * @param[in] f File - * @param[in] dbfile File-name of database. This is a local file - * @param[in] pattern Key regexp, eg "^.*$" - */ -int -xmldb_dump(FILE *f, - char *dbfilename, - char *pattern) -{ - int retval = -1; - - if (_xa_api == NULL){ - clicon_err(OE_DB, 0, "No xmldb plugin"); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - if (_xa_api->xa_dump_fn == NULL){ - clicon_err(OE_DB, 0, "No xmldb function"); - goto done; - } - retval = _xa_api->xa_dump_fn(f, dbfilename, pattern); + retval = _xa_api->xa_put_fn(xh, db, op, api_path, xt); done: return retval; } @@ -209,9 +367,12 @@ xmldb_dump(FILE *f, * @retval 0 OK */ int -xmldb_copy(clicon_handle h, char *from, char *to) +xmldb_copy(clicon_handle h, + char *from, + char *to) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -221,7 +382,11 @@ xmldb_copy(clicon_handle h, char *from, char *to) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_copy_fn(h, from, to); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_copy_fn(xh, from, to); done: return retval; } @@ -234,9 +399,12 @@ xmldb_copy(clicon_handle h, char *from, char *to) * @retval 0 OK */ int -xmldb_lock(clicon_handle h, char *db, int pid) +xmldb_lock(clicon_handle h, + char *db, + int pid) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -246,7 +414,11 @@ xmldb_lock(clicon_handle h, char *db, int pid) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_lock_fn(h, db, pid); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_lock_fn(xh, db, pid); done: return retval; } @@ -260,9 +432,12 @@ xmldb_lock(clicon_handle h, char *db, int pid) * Assume all sanity checks have been made */ int -xmldb_unlock(clicon_handle h, char *db, int pid) +xmldb_unlock(clicon_handle h, + char *db, + int pid) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -272,7 +447,11 @@ xmldb_unlock(clicon_handle h, char *db, int pid) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_unlock_fn(h, db, pid); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_unlock_fn(xh, db, pid); done: return retval; } @@ -284,9 +463,11 @@ xmldb_unlock(clicon_handle h, char *db, int pid) * @retval 0 OK */ int -xmldb_unlock_all(clicon_handle h, int pid) +xmldb_unlock_all(clicon_handle h, + int pid) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -296,7 +477,11 @@ xmldb_unlock_all(clicon_handle h, int pid) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval =_xa_api->xa_unlock_all_fn(h, pid); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval =_xa_api->xa_unlock_all_fn(xh, pid); done: return retval; } @@ -309,9 +494,11 @@ xmldb_unlock_all(clicon_handle h, int pid) * @retval >0 Id of locker */ int -xmldb_islocked(clicon_handle h, char *db) +xmldb_islocked(clicon_handle h, + char *db) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -321,7 +508,11 @@ xmldb_islocked(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval =_xa_api->xa_islocked_fn(h, db); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval =_xa_api->xa_islocked_fn(xh, db); done: return retval; } @@ -334,9 +525,11 @@ xmldb_islocked(clicon_handle h, char *db) * @retval 1 Yes it exists */ int -xmldb_exists(clicon_handle h, char *db) +xmldb_exists(clicon_handle h, + char *db) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -346,7 +539,11 @@ xmldb_exists(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_exists_fn(h, db); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_exists_fn(xh, db); done: return retval; } @@ -358,9 +555,11 @@ xmldb_exists(clicon_handle h, char *db) * @retval 0 OK */ int -xmldb_delete(clicon_handle h, char *db) +xmldb_delete(clicon_handle h, + char *db) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -370,21 +569,27 @@ xmldb_delete(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_delete_fn(h, db); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_delete_fn(xh, db); done: return retval; } -/*! Initialize database +/*! Initialize database. Open database for writing. * @param[in] h Clicon handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -xmldb_init(clicon_handle h, char *db) +xmldb_init(clicon_handle h, + char *db) { - int retval = -1; + int retval = -1; + xmldb_handle xh; if (_xa_api == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); @@ -394,7 +599,11 @@ xmldb_init(clicon_handle h, char *db) clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - retval = _xa_api->xa_init_fn(h, db); + if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + clicon_err(OE_DB, 0, "Not connected to datastore plugin"); + goto done; + } + retval = _xa_api->xa_init_fn(xh, db); done: return retval; } diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index c08deb3a..f0609294 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1074,3 +1074,176 @@ xmlkeyfmt2xpath(char *xkfmt, cbuf_free(cb); return retval; } + +/*! Prune everything that has not been marked + * @param[in] xt XML tree with some node marked + * @param[out] upmark Set if a child (recursively) has marked set. + * The function removes all branches that does not contain a marked child + * XXX: maybe key leafs should not be purged if list is not purged? + * XXX: consider move to clicon_xml + */ +int +xml_tree_prune_unmarked(cxobj *xt, + int *upmark) +{ + int retval = -1; + int submark; + int mark; + cxobj *x; + cxobj *xprev; + + mark = 0; + x = NULL; + xprev = x = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + if (xml_flag(x, XML_FLAG_MARK)){ + mark++; + xprev = x; + continue; /* mark and stop here */ + } + if (xml_tree_prune_unmarked(x, &submark) < 0) + goto done; + if (submark) + mark++; + else{ /* Safe with xml_child_each if last */ + if (xml_purge(x) < 0) + goto done; + x = xprev; + } + xprev = x; + } + retval = 0; + done: + if (upmark) + *upmark = mark; + return retval; +} + +/*! Add default values (if not set) + * @param[in] xt XML tree with some node marked + */ +int +xml_default(cxobj *xt, + void *arg) +{ + int retval = -1; + yang_stmt *ys; + yang_stmt *y; + int i; + cxobj *xc; + cxobj *xb; + char *str; + + ys = (yang_stmt*)xml_spec(xt); + /* Check leaf defaults */ + if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){ + for (i=0; iys_len; i++){ + y = ys->ys_stmt[i]; + if (y->ys_keyword != Y_LEAF) + continue; + assert(y->ys_cv); + if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */ + if (!xml_find(xt, y->ys_argument)){ + if ((xc = xml_new_spec(y->ys_argument, xt, y)) == NULL) + goto done; + if ((xb = xml_new("body", xc)) == NULL) + goto done; + xml_type_set(xb, CX_BODY); + if ((str = cv2str_dup(y->ys_cv)) == NULL){ + clicon_err(OE_UNIX, errno, "cv2str_dup"); + goto done; + } + if (xml_value_set(xb, str) < 0) + goto done; + free(str); + } + } + } + } + retval = 0; + done: + return retval; +} + +/*! Order XML children according to YANG + * @param[in] xt XML top of tree + */ +int +xml_order(cxobj *xt, + void *arg) +{ + int retval = -1; + yang_stmt *y; + yang_stmt *yc; + int i; + int j0; + int j; + cxobj *xc; + cxobj *xj; + char *yname; /* yang child name */ + char *xname; /* xml child name */ + + y = (yang_stmt*)xml_spec(xt); + j0 = 0; + /* Go through xml children and ensure they are same order as yspec children */ + for (i=0; iys_len; i++){ + yc = y->ys_stmt[i]; + if (!yang_is_syntax(yc)) + continue; + yname = yc->ys_argument; + /* First go thru xml children with same name */ + for (; j0ys_argument, name)==NULL){ + clicon_err(OE_XML, 0, "xml node name '%s' does not match yang spec arg '%s'", + name, ys->ys_argument); + goto done; + } + retval = 0; + done: + return retval; +} + diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index af1f33ea..b8e421e0 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -1540,11 +1540,11 @@ yang_parse1(clicon_handle h, /*! Parse top yang module including all its sub-modules. Expand and populate yang tree * - * @param h CLICON handle - * @param yang_dir Directory where all YANG module files reside - * @param module Name of main YANG module. More modules may be parsed if imported - * @param revision Optional module revision date - * @param ysp Yang specification. Should ave been created by caller using yspec_new + * @param[in] h CLICON handle + * @param[in] yang_dir Directory where all YANG module files reside + * @param[in] module Name of main YANG module. More modules may be parsed if imported + * @param[in] revision Optional module revision date + * @param[out] ysp Yang specification. Should ave been created by caller using yspec_new * @retval 0 Everything OK * @retval -1 Error encountered * The database symbols are inserted in alphabetical order. From d8fa7bc033fdbdf500723554c745abed4943be93 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 15 Apr 2017 14:01:06 +0200 Subject: [PATCH 07/24] retval --- lib/src/clixon_xml.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 22a575a3..55109dbb 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -1016,10 +1016,11 @@ clicon_xml_parse_file(int fd, cxobj **cx, char *endtag) { + int retval = -1; + int ret; int len = 0; char ch; - int retval; - char *xmlbuf; + char *xmlbuf = NULL; char *ptr; int maxbuf = BUFLEN; int endtaglen = strlen(endtag); @@ -1038,23 +1039,22 @@ clicon_xml_parse_file(int fd, memset(xmlbuf, 0, maxbuf); ptr = xmlbuf; while (1){ - if ((retval = read(fd, &ch, 1)) < 0){ + if ((ret = read(fd, &ch, 1)) < 0){ clicon_err(OE_XML, errno, "%s: read: [pid:%d]\n", __FUNCTION__, (int)getpid()); break; } - if (retval != 0){ + if (ret != 0){ state = FSM(endtag, ch, state); xmlbuf[len++] = ch; } - if (retval == 0 || state == endtaglen){ + if (ret == 0 || state == endtaglen){ state = 0; if ((*cx = xml_new("top", NULL)) == NULL) break; if (xml_parse(ptr, *cx) < 0){ goto done; - return -1; } break; } From 540cd96e74bef07bd54221d42dc192e0254338d4 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 15 Apr 2017 19:42:35 +0200 Subject: [PATCH 08/24] datastore handles --- apps/backend/backend_main.c | 25 ++-- apps/backend/backend_plugin.c | 2 +- apps/backend/backend_plugin.h | 2 +- apps/backend/backend_socket.c | 4 +- apps/backend/backend_socket.h | 4 +- apps/backend/clixon_backend_handle.c | 1 - apps/cli/cli_handle.c | 1 - datastore/datastore_client.c | 12 +- datastore/keyvalue/clixon_qdb.c | 2 +- lib/clixon/clixon.h.in | 3 +- lib/clixon/clixon_handle.h | 6 - lib/clixon/clixon_options.h | 12 ++ lib/src/clixon_handle.c | 29 +--- lib/src/clixon_hash.c | 9 +- lib/src/clixon_options.c | 106 +++++++++++++- lib/src/clixon_plugin.c | 6 +- lib/src/clixon_proto_client.c | 1 + lib/src/clixon_xml_db.c | 204 +++++++++++++++++---------- lib/src/clixon_xml_map.c | 1 + lib/src/clixon_yang.c | 1 + lib/src/clixon_yang_type.c | 1 + 21 files changed, 286 insertions(+), 146 deletions(-) diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index badf84f2..a8d54d97 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -77,7 +77,7 @@ /*! Terminate. Cannot use h after this */ static int -config_terminate(clicon_handle h) +backend_terminate(clicon_handle h) { yang_spec *yspec; char *pidfile = clicon_backend_pidfile(h); @@ -91,6 +91,7 @@ config_terminate(clicon_handle h) unlink(pidfile); if (sockpath) unlink(sockpath); + xmldb_plugin_unload(h); /* unload storage plugin */ backend_handle_exit(h); /* Cannot use h after this */ event_exit(); clicon_log_register_callback(NULL, NULL); @@ -101,7 +102,7 @@ config_terminate(clicon_handle h) /*! Unlink pidfile and quit */ static void -config_sig_term(int arg) +backend_sig_term(int arg) { static int i=0; @@ -238,11 +239,11 @@ server_socket(clicon_handle h) int ss; /* Open control socket */ - if ((ss = config_socket_init(h)) < 0) + if ((ss = backend_socket_init(h)) < 0) return -1; /* ss is a server socket that the clients connect to. The callback therefore accepts clients on ss */ - if (event_reg_fd(ss, config_accept_client, h, "server socket") < 0) { + if (event_reg_fd(ss, backend_accept_client, h, "server socket") < 0) { close(ss); return -1; } @@ -254,9 +255,9 @@ server_socket(clicon_handle h) * log event. */ static int -config_log_cb(int level, - char *msg, - void *arg) +backend_log_cb(int level, + char *msg, + void *arg) { int retval = -1; size_t n; @@ -320,7 +321,7 @@ main(int argc, char **argv) /* Initiate CLICON handle */ if ((h = backend_handle_init()) == NULL) return -1; - if (config_plugin_init(h) != 0) + if (backend_plugin_init(h) != 0) return -1; foreground = 0; once = 0; @@ -623,14 +624,14 @@ main(int argc, char **argv) goto done; /* Register log notifications */ - if (clicon_log_register_callback(config_log_cb, h) < 0) + if (clicon_log_register_callback(backend_log_cb, h) < 0) goto done; clicon_log(LOG_NOTICE, "%s: %u Started", __PROGRAM__, getpid()); - if (set_signal(SIGTERM, config_sig_term, NULL) < 0){ + if (set_signal(SIGTERM, backend_sig_term, NULL) < 0){ clicon_err(OE_DEMON, errno, "Setting signal"); goto done; } - if (set_signal(SIGINT, config_sig_term, NULL) < 0){ + if (set_signal(SIGINT, backend_sig_term, NULL) < 0){ clicon_err(OE_DEMON, errno, "Setting signal"); goto done; } @@ -646,7 +647,7 @@ main(int argc, char **argv) goto done; done: clicon_log(LOG_NOTICE, "%s: %u Terminated", __PROGRAM__, getpid()); - config_terminate(h); /* Cannot use h after this */ + backend_terminate(h); /* Cannot use h after this */ return 0; } diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 7f0551b6..f336701c 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -132,7 +132,7 @@ config_find_plugin(clicon_handle h, * @retval -1 Error */ int -config_plugin_init(clicon_handle h) +backend_plugin_init(clicon_handle h) { find_plugin_t *fp = config_find_plugin; clicon_hash_t *data = clicon_data(h); diff --git a/apps/backend/backend_plugin.h b/apps/backend/backend_plugin.h index ca6e474e..459ff93b 100644 --- a/apps/backend/backend_plugin.h +++ b/apps/backend/backend_plugin.h @@ -62,7 +62,7 @@ typedef struct { /* * Prototypes */ -int config_plugin_init(clicon_handle h); +int backend_plugin_init(clicon_handle h); int plugin_initiate(clicon_handle h); int plugin_finish(clicon_handle h); diff --git a/apps/backend/backend_socket.c b/apps/backend/backend_socket.c index b83f7ce3..2324de82 100644 --- a/apps/backend/backend_socket.c +++ b/apps/backend/backend_socket.c @@ -174,7 +174,7 @@ config_socket_init_unix(clicon_handle h, char *sock) } int -config_socket_init(clicon_handle h) +backend_socket_init(clicon_handle h) { char *sock; @@ -197,7 +197,7 @@ config_socket_init(clicon_handle h) * XXX: credentials not properly implemented */ int -config_accept_client(int fd, +backend_accept_client(int fd, void *arg) { int retval = -1; diff --git a/apps/backend/backend_socket.h b/apps/backend/backend_socket.h index 735816b9..b88efb89 100644 --- a/apps/backend/backend_socket.h +++ b/apps/backend/backend_socket.h @@ -40,7 +40,7 @@ /* * Prototypes */ -int config_socket_init(clicon_handle h); -int config_accept_client(int fd, void *arg); +int backend_socket_init(clicon_handle h); +int backend_accept_client(int fd, void *arg); #endif /* _BACKEND_SOCKET_H_ */ diff --git a/apps/backend/clixon_backend_handle.c b/apps/backend/clixon_backend_handle.c index 85610654..be2cb07c 100644 --- a/apps/backend/clixon_backend_handle.c +++ b/apps/backend/clixon_backend_handle.c @@ -85,7 +85,6 @@ struct backend_handle { int bh_magic; /* magic (HDR)*/ clicon_hash_t *bh_copt; /* clicon option list (HDR) */ clicon_hash_t *bh_data; /* internal clicon data (HDR) */ - void *bh_xmldb; /* XMLDB storage handle, uie xmldb_handle */ /* ------ end of common handle ------ */ struct client_entry *bh_ce_list; /* The client list */ int bh_ce_nr; /* Number of clients, just increment */ diff --git a/apps/cli/cli_handle.c b/apps/cli/cli_handle.c index bc63a179..7d2e422e 100644 --- a/apps/cli/cli_handle.c +++ b/apps/cli/cli_handle.c @@ -79,7 +79,6 @@ struct cli_handle { int cl_magic; /* magic (HDR)*/ clicon_hash_t *cl_copt; /* clicon option list (HDR) */ clicon_hash_t *cl_data; /* internal clicon data (HDR) */ - void *cl_xmldb; /* XMLDB storage handle, uie xmldb_handle */ /* ------ end of common handle ------ */ cligen_handle cl_cligen; /* cligen handle */ diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c index c43303f7..7445efc0 100644 --- a/datastore/datastore_client.c +++ b/datastore/datastore_client.c @@ -31,6 +31,14 @@ ***** END LICENSE BLOCK ***** + * Examples: + +./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip get / + +sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip put merge /interfaces/interface=eth0 +eth66 + + * */ #ifdef HAVE_CONFIG_H @@ -221,8 +229,8 @@ main(int argc, char **argv) goto done; if (xmldb_put(h, db, op, argv[2], xn) < 0) goto done; - if (xt) - xml_free(xt); + if (xn) + xml_free(xn); } else if (strcmp(cmd, "copy")==0){ if (argc < 3) diff --git a/datastore/keyvalue/clixon_qdb.c b/datastore/keyvalue/clixon_qdb.c index 1af75ab0..5bfa19a5 100644 --- a/datastore/keyvalue/clixon_qdb.c +++ b/datastore/keyvalue/clixon_qdb.c @@ -424,7 +424,7 @@ db_regexp(char *file, /* Retrieve value if required */ if ( ! noval) { if((val = dpget(iterdp, key, -1, 0, -1, &vlen)) == NULL) { - clicon_log(OE_DB, "%s: dpget: %s", __FUNCTION__, dperrmsg(dpecode)); + clicon_log(LOG_WARNING, "%s: dpget: %s", __FUNCTION__, dperrmsg(dpecode)); goto quit; } } diff --git a/lib/clixon/clixon.h.in b/lib/clixon/clixon.h.in index d9f00a80..26220770 100644 --- a/lib/clixon/clixon.h.in +++ b/lib/clixon/clixon.h.in @@ -76,13 +76,12 @@ #include #include #include +#include #include #include #include #include #include -#include -#include /* * Global variables generated by Makefile diff --git a/lib/clixon/clixon_handle.h b/lib/clixon/clixon_handle.h index c4f601b6..207d076f 100644 --- a/lib/clixon/clixon_handle.h +++ b/lib/clixon/clixon_handle.h @@ -71,10 +71,4 @@ clicon_hash_t *clicon_options(clicon_handle h); /* Return internal clicon data (hash-array) given a handle.*/ clicon_hash_t *clicon_data(clicon_handle h); -/* Set or reset XMLDB storage handle */ -int clicon_handle_xmldb_set(clicon_handle h, void *xh); /* ie xmldb_handle */ - -/* Get XMLDB storage handle */ -void *clicon_handle_xmldb_get(clicon_handle h); - #endif /* _CLIXON_HANDLE_H_ */ diff --git a/lib/clixon/clixon_options.h b/lib/clixon/clixon_options.h index 7c776c13..7f3bfbc4 100644 --- a/lib/clixon/clixon_options.h +++ b/lib/clixon/clixon_options.h @@ -120,4 +120,16 @@ int clicon_dbspec_yang_set(clicon_handle h, struct yang_spec *ys); char *clicon_dbspec_name(clicon_handle h); int clicon_dbspec_name_set(clicon_handle h, char *name); +int clicon_xmldb_plugin_set(clicon_handle h, plghndl_t handle); + +plghndl_t clicon_xmldb_plugin_get(clicon_handle h); + +int clicon_xmldb_api_set(clicon_handle h, void *xa_api); + +void *clicon_xmldb_api_get(clicon_handle h); + +int clicon_xmldb_handle_set(clicon_handle h, void *xh); + +void *clicon_xmldb_handle_get(clicon_handle h); + #endif /* _CLIXON_OPTIONS_H_ */ diff --git a/lib/src/clixon_handle.c b/lib/src/clixon_handle.c index 47accd83..f5debeff 100644 --- a/lib/src/clixon_handle.c +++ b/lib/src/clixon_handle.c @@ -52,6 +52,7 @@ #include "clixon_handle.h" #include "clixon_err.h" #include "clixon_yang.h" +#include "clixon_plugin.h" #include "clixon_options.h" #define CLICON_MAGIC 0x99aafabe @@ -66,7 +67,6 @@ struct clicon_handle { int ch_magic; /* magic (HDR) */ clicon_hash_t *ch_copt; /* clicon option list (HDR) */ clicon_hash_t *ch_data; /* internal clicon data (HDR) */ - void *ch_xmldb; /* XMLDB storage handle, uie xmldb_handle */ }; /*! Internal call to allocate a CLICON handle. @@ -166,30 +166,3 @@ clicon_data(clicon_handle h) return ch->ch_data; } -/*! Set or reset XMLDB storage handle - * @param[in] h Clicon handle - * @param[in] xh XMLDB storage handle. If NULL reset it - * @note Just keep note of it, dont allocate it or so. - */ -int -clicon_handle_xmldb_set(clicon_handle h, - void *xh) -{ - struct clicon_handle *ch = handle(h); - - ch->ch_xmldb = xh; - return 0; -} - -/*! Get XMLDB storage handle - * @param[in] h Clicon handle - * @retval xh XMLDB storage handle. If not connected return NULL - */ -void * -clicon_handle_xmldb_get(clicon_handle h) - -{ - struct clicon_handle *ch = handle(h); - - return ch->ch_xmldb; -} diff --git a/lib/src/clixon_hash.c b/lib/src/clixon_hash.c index c3f6d71d..fc914b60 100644 --- a/lib/src/clixon_hash.c +++ b/lib/src/clixon_hash.c @@ -200,8 +200,8 @@ hash_value(clicon_hash_t *hash, /*! Copy value and add hash entry. * * @param[in] hash Hash table - * @param[in] key New variable name - * @param[in] val New variable value + * @param[in] key Variable name + * @param[in] val Variable value * @param[in] vlen Length of variable value * @retval variable New hash structure on success * @retval NULL Failure @@ -212,8 +212,9 @@ hash_add(clicon_hash_t *hash, void *val, size_t vlen) { - void *newval; - clicon_hash_t h, new = NULL; + void *newval; + clicon_hash_t h; + clicon_hash_t new = NULL; /* If variable exist, don't allocate a new. just replace value */ h = hash_lookup (hash, key); diff --git a/lib/src/clixon_options.c b/lib/src/clixon_options.c index 1414977a..ecd6863a 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -63,6 +63,7 @@ #include "clixon_handle.h" #include "clixon_log.h" #include "clixon_yang.h" +#include "clixon_plugin.h" #include "clixon_options.h" /* @@ -625,12 +626,12 @@ clicon_dbspec_yang(clicon_handle h) return NULL; } -/* - * Set dbspec (YANG variant) +/*! Set yang database specification * ys must be a malloced pointer */ int -clicon_dbspec_yang_set(clicon_handle h, struct yang_spec *ys) +clicon_dbspec_yang_set(clicon_handle h, + struct yang_spec *ys) { clicon_hash_t *cdat = clicon_data(h); @@ -642,8 +643,7 @@ clicon_dbspec_yang_set(clicon_handle h, struct yang_spec *ys) return 0; } -/* - * Get dbspec name as read from spec. Can be used in CLI '@' syntax. +/*! Get dbspec name as read from spec. Can be used in CLI '@' syntax. * XXX: this we muśt change,... */ char * @@ -654,11 +654,103 @@ clicon_dbspec_name(clicon_handle h) return clicon_option_str(h, "dbspec_name"); } -/* - * Set dbspec name as read from spec. Can be used in CLI '@' syntax. +/*! Set dbspec name as read from spec. Can be used in CLI '@' syntax. */ int clicon_dbspec_name_set(clicon_handle h, char *name) { return clicon_option_str_set(h, "dbspec_name", name); } + +/*! Set xmldb datastore plugin handle, as used by dlopen/dlsym/dlclose */ +int +clicon_xmldb_plugin_set(clicon_handle h, + plghndl_t handle) +{ + clicon_hash_t *cdat = clicon_data(h); + + if (hash_add(cdat, "xmldb_plugin", &handle, sizeof(void*)) == NULL) + return -1; + return 0; +} + +/*! Get xmldb datastore plugin handle, as used by dlopen/dlsym/dlclose */ +plghndl_t +clicon_xmldb_plugin_get(clicon_handle h) +{ + clicon_hash_t *cdat = clicon_data(h); + size_t len; + void *p; + + if ((p = hash_value(cdat, "xmldb_plugin", &len)) != NULL) + return *(plghndl_t*)p; + return NULL; +} + +/*! Set or reset XMLDB API struct pointer + * @param[in] h Clicon handle + * @param[in] xa XMLDB API struct + * @note xa is really of type struct xmldb_api* + */ +int +clicon_xmldb_api_set(clicon_handle h, + void *xa) +{ + clicon_hash_t *cdat = clicon_data(h); + + /* It is the pointer to xa_api that should be copied by hash, + so we send a ptr to the ptr to indicate what to copy. + */ + if (hash_add(cdat, "xmldb_api", &xa, sizeof(void*)) == NULL) + return -1; + return 0; +} + +/*! Get XMLDB API struct pointer + * @param[in] h Clicon handle + * @retval xa XMLDB API struct + * @note xa is really of type struct xmldb_api* + */ +void * +clicon_xmldb_api_get(clicon_handle h) +{ + clicon_hash_t *cdat = clicon_data(h); + size_t len; + void *xa; + + if ((xa = hash_value(cdat, "xmldb_api", &len)) != NULL) + return *(void**)xa; + return NULL; +} + +/*! Set or reset XMLDB storage handle + * @param[in] h Clicon handle + * @param[in] xh XMLDB storage handle. If NULL reset it + * @note Just keep note of it, dont allocate it or so. + */ +int +clicon_xmldb_handle_set(clicon_handle h, + void *xh) +{ + clicon_hash_t *cdat = clicon_data(h); + + if (hash_add(cdat, "xmldb_handle", &xh, sizeof(void*)) == NULL) + return -1; + return 0; +} + +/*! Get XMLDB storage handle + * @param[in] h Clicon handle + * @retval xh XMLDB storage handle. If not connected return NULL + */ +void * +clicon_xmldb_handle_get(clicon_handle h) +{ + clicon_hash_t *cdat = clicon_data(h); + size_t len; + void *xh; + + if ((xh = hash_value(cdat, "xmldb_handle", &len)) != NULL) + return *(void**)xh; + return NULL; +} diff --git a/lib/src/clixon_plugin.c b/lib/src/clixon_plugin.c index c127a948..729ada7d 100644 --- a/lib/src/clixon_plugin.c +++ b/lib/src/clixon_plugin.c @@ -93,9 +93,9 @@ clicon_find_func(clicon_handle h, char *plugin, char *func) * @param[in] dlflags See man(3) dlopen */ plghndl_t -plugin_load (clicon_handle h, - char *file, - int dlflags) +plugin_load(clicon_handle h, + char *file, + int dlflags) { char *error; void *handle = NULL; diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c index d5a7e977..03558c57 100644 --- a/lib/src/clixon_proto_client.c +++ b/lib/src/clixon_proto_client.c @@ -60,6 +60,7 @@ #include "clixon_hash.h" #include "clixon_handle.h" #include "clixon_yang.h" +#include "clixon_plugin.h" #include "clixon_options.h" #include "clixon_xml.h" #include "clixon_xsl.h" diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 97ef6dd3..8abfd12b 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -59,17 +59,15 @@ #include "clixon_handle.h" #include "clixon_xml.h" #include "clixon_yang.h" +#include "clixon_plugin.h" #include "clixon_options.h" #include "clixon_xml_db.h" -static struct xmldb_api *_xa_api = NULL; - -/*! Load a specific plugin, call its init function and add it to plugins list +/*! Load an xmldb storage plugin according to filename * If init function fails (not found, wrong version, etc) print a log and dont * add it. - * @param[in] name Filename (complete path) of plugin + * @param[in] h CLicon handle * @param[in] filename Actual filename with path - * @param[out] plugin Plugin data structure for invoking. Dealloc with free */ int xmldb_plugin_load(clicon_handle h, @@ -78,8 +76,9 @@ xmldb_plugin_load(clicon_handle h, int retval = -1; char *dlerrcode; plugin_init_t *initfun; - void *handle = NULL; + plghndl_t handle = NULL; char *error; + struct xmldb_api *xa = NULL; dlerror(); /* Clear any existing error */ if ((handle = dlopen(filename, RTLD_NOW|RTLD_GLOBAL)) == NULL) { @@ -94,21 +93,27 @@ xmldb_plugin_load(clicon_handle h, XMLDB_PLUGIN_INIT_FN, dlerrcode); goto fail; } - if ((_xa_api = initfun(XMLDB_API_VERSION)) == NULL) { + if ((xa = initfun(XMLDB_API_VERSION)) == NULL) { clicon_log(LOG_WARNING, "%s: failed when running init function %s: %s", filename, XMLDB_PLUGIN_INIT_FN, errno?strerror(errno):""); goto fail; } - if (_xa_api->xa_version != XMLDB_API_VERSION){ + if (xa->xa_version != XMLDB_API_VERSION){ clicon_log(LOG_WARNING, "%s: Unexpected plugin version number: %d", - filename, _xa_api->xa_version); + filename, xa->xa_version); goto fail; } - if (_xa_api->xa_magic != XMLDB_API_MAGIC){ + if (xa->xa_magic != XMLDB_API_MAGIC){ clicon_log(LOG_WARNING, "%s: Wrong plugin magic number: %x", - filename, _xa_api->xa_magic); + filename, xa->xa_magic); goto fail; } + /* Add plugin */ + if (clicon_xmldb_plugin_set(h, handle) < 0) + goto done; + /* Add API */ + if (clicon_xmldb_api_set(h, xa) < 0) + goto done; clicon_log(LOG_WARNING, "xmldb plugin %s loaded", filename); retval = 0; done: @@ -120,11 +125,41 @@ xmldb_plugin_load(clicon_handle h, goto done; } -/*! XXX: fixme */ +/*! Unload the xmldb storage plugin */ int xmldb_plugin_unload(clicon_handle h) { - return 0; + int retval = -1; + plghndl_t handle; + struct xmldb_api *xa; + xmldb_handle xh; + char *error; + + if ((handle = clicon_xmldb_plugin_get(h)) == NULL){ + clicon_err(OE_PLUGIN, errno, "No plugin handle"); + goto done; + } + /* If connected storage handle then disconnect */ + if ((xh = clicon_xmldb_handle_get(h)) != NULL) + xmldb_disconnect(h); /* sets xmldb handle to NULL */ + /* Deregister api */ + if ((xa = clicon_xmldb_api_get(h)) != NULL){ + /* Call plugin_exit */ + if (xa->xa_plugin_exit_fn != NULL) + xa->xa_plugin_exit_fn(); + /* Deregister API (it is allocated in plugin) */ + clicon_xmldb_api_set(h, NULL); + } + /* Unload plugin */ + dlerror(); /* Clear any existing error */ + if (dlclose(handle) != 0) { + error = (char*)dlerror(); + clicon_err(OE_PLUGIN, errno, "dlclose: %s\n", error ? error : "Unknown error"); + /* Just report no -1 return*/ + } + retval = 0; + done: + return retval; } /*! Connect to a datastore plugin @@ -139,21 +174,21 @@ xmldb_plugin_unload(clicon_handle h) int xmldb_connect(clicon_handle h) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_connect_fn == NULL){ + if (xa->xa_connect_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = _xa_api->xa_connect_fn()) == NULL) + if ((xh = xa->xa_connect_fn()) == NULL) goto done; - clicon_handle_xmldb_set(h, xh); - + clicon_xmldb_handle_set(h, xh); retval = 0; done: return retval; @@ -168,22 +203,23 @@ xmldb_disconnect(clicon_handle h) { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_disconnect_fn == NULL){ + if (xa->xa_disconnect_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Already disconnected from datastore plugin"); goto done; } - if (_xa_api->xa_disconnect_fn(xh) < 0) + if (xa->xa_disconnect_fn(xh) < 0) goto done; - clicon_handle_xmldb_set(h, NULL); + clicon_xmldb_handle_set(h, NULL); retval = 0; done: return retval; @@ -203,20 +239,21 @@ xmldb_getopt(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_getopt_fn == NULL){ + if (xa->xa_getopt_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_getopt_fn(xh, optname, value); + retval = xa->xa_getopt_fn(xh, optname, value); done: return retval; } @@ -235,20 +272,21 @@ xmldb_setopt(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_setopt_fn == NULL){ + if (xa->xa_setopt_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_setopt_fn(xh, optname, value); + retval = xa->xa_setopt_fn(xh, optname, value); done: return retval; } @@ -293,20 +331,22 @@ xmldb_get(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_get_fn == NULL){ + if (xa->xa_get_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_get_fn(xh, db, xpath, xtop, xvec, xlen); + clicon_log(LOG_WARNING, "%s: db:%s xpath:%s", __FUNCTION__, db, xpath); + retval = xa->xa_get_fn(xh, db, xpath, xtop, xvec, xlen); done: return retval; } @@ -341,20 +381,30 @@ xmldb_put(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_put_fn == NULL){ + if (xa->xa_put_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_put_fn(xh, db, op, api_path, xt); + { + cbuf *cb = cbuf_new(); + if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) + goto done; + + clicon_log(LOG_WARNING, "%s: db:%s op:%d api_path:%s xml:%s", __FUNCTION__, + db, op, api_path, cbuf_get(cb)); + cbuf_free(cb); + } + retval = xa->xa_put_fn(xh, db, op, api_path, xt); done: return retval; } @@ -373,20 +423,21 @@ xmldb_copy(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_copy_fn == NULL){ + if (xa->xa_copy_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_copy_fn(xh, from, to); + retval = xa->xa_copy_fn(xh, from, to); done: return retval; } @@ -405,20 +456,21 @@ xmldb_lock(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_lock_fn == NULL){ + if (xa->xa_lock_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_lock_fn(xh, db, pid); + retval = xa->xa_lock_fn(xh, db, pid); done: return retval; } @@ -438,20 +490,21 @@ xmldb_unlock(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_unlock_fn == NULL){ + if (xa->xa_unlock_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_unlock_fn(xh, db, pid); + retval = xa->xa_unlock_fn(xh, db, pid); done: return retval; } @@ -468,20 +521,21 @@ xmldb_unlock_all(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_unlock_all_fn == NULL){ + if (xa->xa_unlock_all_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval =_xa_api->xa_unlock_all_fn(xh, pid); + retval =xa->xa_unlock_all_fn(xh, pid); done: return retval; } @@ -499,20 +553,21 @@ xmldb_islocked(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_islocked_fn == NULL){ + if (xa->xa_islocked_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval =_xa_api->xa_islocked_fn(xh, db); + retval =xa->xa_islocked_fn(xh, db); done: return retval; } @@ -530,20 +585,21 @@ xmldb_exists(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_exists_fn == NULL){ + if (xa->xa_exists_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_exists_fn(xh, db); + retval = xa->xa_exists_fn(xh, db); done: return retval; } @@ -560,20 +616,21 @@ xmldb_delete(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_delete_fn == NULL){ + if (xa->xa_delete_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_delete_fn(xh, db); + retval = xa->xa_delete_fn(xh, db); done: return retval; } @@ -590,20 +647,21 @@ xmldb_init(clicon_handle h, { int retval = -1; xmldb_handle xh; + struct xmldb_api *xa; - if (_xa_api == NULL){ + if ((xa = clicon_xmldb_api_get(h)) == NULL){ clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (_xa_api->xa_init_fn == NULL){ + if (xa->xa_init_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } - if ((xh = clicon_handle_xmldb_get(h)) == NULL){ + if ((xh = clicon_xmldb_handle_get(h)) == NULL){ clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = _xa_api->xa_init_fn(xh, db); + retval = xa->xa_init_fn(xh, db); done: return retval; } diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index f0609294..9ac928a6 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -79,6 +79,7 @@ #include "clixon_string.h" #include "clixon_yang.h" #include "clixon_yang_type.h" +#include "clixon_plugin.h" #include "clixon_options.h" #include "clixon_xml.h" #include "clixon_xsl.h" diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index b8e421e0..14a03e84 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -66,6 +66,7 @@ #include "clixon_file.h" #include "clixon_yang.h" #include "clixon_hash.h" +#include "clixon_plugin.h" #include "clixon_options.h" #include "clixon_yang_type.h" #include "clixon_yang_parse.h" diff --git a/lib/src/clixon_yang_type.c b/lib/src/clixon_yang_type.c index dbb5ac65..6533ff7d 100644 --- a/lib/src/clixon_yang_type.c +++ b/lib/src/clixon_yang_type.c @@ -63,6 +63,7 @@ #include "clixon_handle.h" #include "clixon_yang.h" #include "clixon_hash.h" +#include "clixon_plugin.h" #include "clixon_options.h" #include "clixon_yang.h" #include "clixon_yang_type.h" From ee9b74d7354f8a2928295121a8688cb81bdf76e9 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 16 Apr 2017 19:06:19 +0200 Subject: [PATCH 09/24] formalizing xmldb api --- apps/backend/backend_client.c | 4 +- apps/backend/backend_main.c | 13 +++-- apps/restconf/restconf_methods.c | 75 +------------------------ datastore/datastore_client.c | 40 +++++++++----- datastore/keyvalue/clixon_keyvalue.c | 77 ++++++++++++++++++-------- datastore/keyvalue/clixon_keyvalue.h | 2 +- datastore/text/clixon_xmldb_text.c | 43 ++++++++++++--- datastore/text/clixon_xmldb_text.h | 2 +- lib/clixon/clixon_xml_db.h | 4 +- lib/clixon/clixon_xml_map.h | 1 + lib/src/clixon_xml.c | 2 +- lib/src/clixon_xml_db.c | 73 ++++++++++++++----------- lib/src/clixon_xml_map.c | 82 ++++++++++++++++++++++++++++ 13 files changed, 252 insertions(+), 166 deletions(-) diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index f347e6f9..591d0e33 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -438,7 +438,7 @@ from_client_unlock(clicon_handle h, goto ok; } else{ - xmldb_unlock(h, db, pid); + xmldb_unlock(h, db); if (cprintf(cbret, "") < 0) goto done; } @@ -496,7 +496,7 @@ from_client_kill_session(clicon_handle h, if (1 || (kill (pid, 0) != 0 && errno == ESRCH)){ /* Nothing there */ /* clear from locks */ if (xmldb_islocked(h, db) == pid) - xmldb_unlock(h, db, pid); + xmldb_unlock(h, db); } else{ /* failed to kill client */ cprintf(cbret, "" diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index a8d54d97..d3973ac6 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -73,7 +73,7 @@ #include "backend_handle.h" /* Command line options to be passed to getopt(3) */ -#define BACKEND_OPTS "hD:f:d:Fzu:P:1IRCc:rg:pty:x:" +#define BACKEND_OPTS "hD:f:d:b:Fzu:P:1IRCc:rg:py:x:" /*! Terminate. Cannot use h after this */ static int @@ -129,6 +129,7 @@ usage(char *argv0, clicon_handle h) " -D \tdebug\n" " -f \tCLICON config file (mandatory)\n" " -d \tSpecify backend plugin directory (default: %s)\n" + " -b \tSpecify XMLDB database directory\n" " -z\t\tKill other config daemon and exit\n" " -F\t\tforeground\n" " -1\t\tonce (dont wait for events)\n" @@ -140,7 +141,6 @@ usage(char *argv0, clicon_handle h) " -c \tLoad specified application config.\n" " -r\t\tReload running database\n" " -p \t\tPrint database yang specification\n" - " -t \t\tPrint alternate spec translation (eg if YANG print KEY, if KEY print YANG)\n" " -g \tClient membership required to this group (default: %s)\n" " -y \tOverride yang spec file (dont include .yang suffix)\n" " -x \tXMLDB plugin\n", @@ -308,7 +308,6 @@ main(int argc, char **argv) clicon_handle h; int help = 0; int printspec = 0; - int printalt = 0; int pid; char *pidfile; char *sock; @@ -387,6 +386,11 @@ main(int argc, char **argv) usage(argv[0], h); clicon_option_str_set(h, "CLICON_BACKEND_DIR", optarg); break; + case 'b': /* XMLDB database directory */ + if (!strlen(optarg)) + usage(argv[0], h); + clicon_option_str_set(h, "CLICON_XMLDB_DIR", optarg); + break; case 'F' : /* foreground */ foreground = 1; break; @@ -425,9 +429,6 @@ main(int argc, char **argv) case 'p' : /* Print spec */ printspec++; break; - case 't' : /* Print alternative dbspec format (eg if YANG, print KEY) */ - printalt++; - break; case 'y' :{ /* yang module */ /* Set revision to NULL, extract dir and module */ char *str = strdup(optarg); diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 8a74a0d6..f3e61858 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -152,86 +152,19 @@ api_data_get_gen(clicon_handle h, int head) { int retval = -1; - cg_var *cv; - char *val; - char *v; - int i; cbuf *path = NULL; - cbuf *path1 = NULL; cbuf *cbx = NULL; cxobj **vec = NULL; yang_spec *yspec; - yang_stmt *y = NULL; - yang_stmt *ykey; - char *name; - cvec *cvk = NULL; /* vector of index keys */ - cg_var *cvi; cxobj *xret = NULL; clicon_debug(1, "%s", __FUNCTION__); yspec = clicon_dbspec_yang(h); if ((path = cbuf_new()) == NULL) goto done; - if ((path1 = cbuf_new()) == NULL) /* without [] qualifiers */ - goto done; - cv = NULL; - cprintf(path1, "/"); - /* translate eg a/b=c -> a/[b=c] */ - for (i=pi; iys_argument); - notfound(r); - goto done; - } - clicon_debug(1, "ykey:%s", ykey->ys_argument); - - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - cvi = NULL; - /* Iterate over individual yang keys */ - cprintf(path, "/%s", name); - v = val; - while ((cvi = cvec_each(cvk, cvi)) != NULL){ - cprintf(path, "[%s=%s]", cv_string_get(cvi), v); - v += strlen(v)+1; - } - if (val) - free(val); - } - else{ - cprintf(path, "%s%s", (i==pi?"":"/"), name); - cprintf(path1, "/%s", name); - } + if (xml_apipath2xpath(yspec, pcvec, pi, path) < 0){ + notfound(r); + goto done; } clicon_debug(1, "%s path:%s", __FUNCTION__, cbuf_get(path)); if (clicon_rpc_get_config(h, "running", cbuf_get(path), &xret) < 0){ @@ -267,8 +200,6 @@ api_data_get_gen(clicon_handle h, cbuf_free(cbx); if (path) cbuf_free(path); - if (path1) - cbuf_free(path1); if (xret) xml_free(xret); return retval; diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c index 7445efc0..2a094e7b 100644 --- a/datastore/datastore_client.c +++ b/datastore/datastore_client.c @@ -35,8 +35,9 @@ ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip get / -sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip put merge /interfaces/interface=eth0 -eth66 +sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip put merge /interfaces/interface=eth66 'eth66' + +sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip put merge / 'eth0true' * */ @@ -89,10 +90,10 @@ usage(char *argv0) "\t-m \tYang module. Mandatory\n" "and command is either:\n" "\tget \n" - "\tput (set|merge|delete) \tXML on stdin\n" - "\tcopy \n" + "\tput (set|merge|delete) \n" + "\tcopy \n" "\tlock \n" - "\tunlock \n" + "\tunlock\n" "\tunlock_all \n" "\tislocked\n" "\texists\n" @@ -212,18 +213,18 @@ main(int argc, char **argv) if (xmldb_setopt(h, "yangspec", yspec) < 0) goto done; if (strcmp(cmd, "get")==0){ - if (argc < 2) + if (argc != 2) usage(argv0); if (xmldb_get(h, db, argv[1], &xt, NULL, 0) < 0) goto done; clicon_xml2file(stdout, xt, 0, 1); } else if (strcmp(cmd, "put")==0){ - if (argc < 3) + if (argc != 4) usage(argv0); if (xml_operation(argv[1], &op) < 0) usage(argv0); - if (clicon_xml_parse_file(0, &xt, "") < 0) + if (clicon_xml_parse_str(argv[3], &xt) < 0) goto done; if (xml_rootchild(xt, 0, &xn) < 0) goto done; @@ -233,50 +234,59 @@ main(int argc, char **argv) xml_free(xn); } else if (strcmp(cmd, "copy")==0){ - if (argc < 3) + if (argc != 2) usage(argv0); - if (xmldb_copy(h, argv[1], argv[2]) < 0) + if (xmldb_copy(h, db, argv[1]) < 0) goto done; } else if (strcmp(cmd, "lock")==0){ - if (argc < 2) + if (argc != 2) usage(argv0); pid = atoi(argv[1]); if (xmldb_lock(h, db, pid) < 0) goto done; } else if (strcmp(cmd, "unlock")==0){ - if (argc < 2) + if (argc != 1) usage(argv0); - pid = atoi(argv[1]); - if (xmldb_unlock(h, db, pid) < 0) + if (xmldb_unlock(h, db) < 0) goto done; } else if (strcmp(cmd, "unlock_all")==0){ - if (argc < 2) + if (argc != 2) usage(argv0); pid = atoi(argv[1]); if (xmldb_unlock_all(h, pid) < 0) goto done; } else if (strcmp(cmd, "islocked")==0){ + if (argc != 1) + usage(argv0); if ((ret = xmldb_islocked(h, db)) < 0) goto done; fprintf(stdout, "islocked: %d\n", ret); } else if (strcmp(cmd, "exists")==0){ + if (argc != 1) + usage(argv0); if ((ret = xmldb_exists(h, db)) < 0) goto done; fprintf(stdout, "exists: %d\n", ret); } else if (strcmp(cmd, "delete")==0){ + if (argc != 1) + usage(argv0); if (xmldb_delete(h, db) < 0) goto done; } else if (strcmp(cmd, "init")==0){ + if (argc != 1) + usage(argv0); if (xmldb_init(h, db) < 0) goto done; } + else + clicon_err(OE_DB, 0, "Unrecognized command: %s", cmd); done: return 0; } diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index ab8565b3..65411349 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -160,7 +160,7 @@ static int _startup_locked = 0; * The filename reside in CLICON_XMLDB_DIR option */ static int -db2file(struct kv_handle *kh, +kv_db2file(struct kv_handle *kh, char *db, char **filename) { @@ -180,7 +180,7 @@ db2file(struct kv_handle *kh, strcmp(db, "candidate") != 0 && strcmp(db, "startup") != 0 && strcmp(db, "tmp") != 0){ - clicon_err(OE_XML, 0, "Unexpected database: %s", db); + clicon_err(OE_XML, 0, "No such database: %s", db); goto done; } cprintf(cb, "%s/%s_db", dir, db); @@ -616,7 +616,7 @@ kv_get(xmldb_handle xh, clicon_debug(2, "%s", __FUNCTION__); - if (db2file(kh, db, &dbfile) < 0) + if (kv_db2file(kh, db, &dbfile) < 0) goto done; if (dbfile==NULL){ clicon_err(OE_XML, 0, "dbfile NULL"); @@ -629,7 +629,7 @@ kv_get(xmldb_handle xh, /* Read in complete database (this can be optimized) */ if ((npairs = db_regexp(dbfile, "", __FUNCTION__, &pairs, 0)) < 0) goto done; - if ((xt = xml_new_spec("clicon", NULL, yspec)) == NULL) + if ((xt = xml_new_spec("config", NULL, yspec)) == NULL) goto done; /* Translate to complete xml tree */ for (i = 0; i < npairs; i++) { @@ -836,7 +836,7 @@ xmldb_put_xkey(struct kv_handle *kh, clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } - if (db2file(kh, db, &filename) < 0) + if (kv_db2file(kh, db, &filename) < 0) goto done; if (xk == NULL || *xk!='/'){ clicon_err(OE_DB, 0, "Invalid key: %s", xk); @@ -852,6 +852,9 @@ xmldb_put_xkey(struct kv_handle *kh, } if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) goto done; + /* Remove trailing '/'. Like in /a/ -> /a */ + if (nvec > 1 && !strlen(vec[nvec-1])) + nvec--; if (nvec < 2){ clicon_err(OE_XML, 0, "Malformed key: %s", xk); goto done; @@ -1066,7 +1069,7 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } - if (db2file(kh, db, &filename) < 0) + if (kv_db2file(kh, db, &filename) < 0) goto done; if (api_path == NULL || *api_path!='/'){ clicon_err(OE_DB, 0, "Invalid api path: %s", api_path); @@ -1082,6 +1085,9 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, } if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL) goto done; + /* Remove trailing '/'. Like in /a/ -> /a */ + if (nvec > 1 && !strlen(vec[nvec-1])) + nvec--; if (nvec < 2){ clicon_err(OE_XML, 0, "Malformed key: %s", api_path); goto done; @@ -1266,13 +1272,14 @@ kv_put(xmldb_handle xh, yang_spec *yspec; char *dbfilename = NULL; - if (xml_child_nr(xt)==0 || xml_body(xt)!= NULL) + if ((xml_child_nr(xt)==0 || xml_body(xt)!= NULL) && + api_path && strlen(api_path) && strcmp(api_path,"/")) return xmldb_put_xkey(kh, db, op, api_path, xml_body(xt)); if ((yspec = kh->kh_yangspec) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } - if (db2file(kh, db, &dbfilename) < 0) + if (kv_db2file(kh, db, &dbfilename) < 0) goto done; if (op == OP_REPLACE){ if (db_delete(dbfilename) < 0) @@ -1281,7 +1288,8 @@ kv_put(xmldb_handle xh, goto done; } while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ - if (api_path && strlen(api_path)){ + /* An api path that is not / */ + if (api_path && strlen(api_path) && strcmp(api_path,"/")){ if (xmldb_put_restconf_api_path(kh, db, op, api_path, x) < 0) goto done; continue; @@ -1323,9 +1331,9 @@ kv_copy(xmldb_handle xh, char *tofile = NULL; /* XXX lock */ - if (db2file(kh, from, &fromfile) < 0) + if (kv_db2file(kh, from, &fromfile) < 0) goto done; - if (db2file(kh, to, &tofile) < 0) + if (kv_db2file(kh, to, &tofile) < 0) goto done; if (clicon_file_copy(fromfile, tofile) < 0) goto done; @@ -1350,16 +1358,25 @@ kv_lock(xmldb_handle xh, char *db, int pid) { + int retval = -1; // struct kv_handle *kh = handle(xh); - + fprintf(stderr, "%s %s %d\n", __FUNCTION__, db, pid); if (strcmp("running", db) == 0) _running_locked = pid; else if (strcmp("candidate", db) == 0) _candidate_locked = pid; else if (strcmp("startup", db) == 0) _startup_locked = pid; + else{ + clicon_err(OE_DB, 0, "No such database: %s", db); + goto done; + } clicon_debug(1, "%s: locked by %u", db, pid); - return 0; + fprintf(stderr, "running:%d candidate:%d startup:%d\n", + _running_locked, _candidate_locked, _startup_locked); + retval = 0; + done: + return retval; } /*! Unlock database @@ -1372,18 +1389,24 @@ kv_lock(xmldb_handle xh, */ int kv_unlock(xmldb_handle xh, - char *db, - int pid) + char *db) { + int retval = -1; // struct kv_handle *kh = handle(xh); - + fprintf(stderr, "%s %s\n", __FUNCTION__, db); if (strcmp("running", db) == 0) _running_locked = 0; else if (strcmp("candidate", db) == 0) _candidate_locked = 0; else if (strcmp("startup", db) == 0) _startup_locked = 0; - return 0; + else{ + clicon_err(OE_DB, 0, "No such database: %s", db); + goto done; + } + retval = 0; + done: + return retval; } /*! Unlock all databases locked by pid (eg process dies) @@ -1418,15 +1441,21 @@ int kv_islocked(xmldb_handle xh, char *db) { + int retval = -1; // struct kv_handle *kh = handle(xh); + fprintf(stderr, "%s %s\n", __FUNCTION__, db); + fprintf(stderr, "running:%d candidate:%d startup:%d\n", + _running_locked, _candidate_locked, _startup_locked); if (strcmp("running", db) == 0) - return (_running_locked); + retval = _running_locked; else if (strcmp("candidate", db) == 0) - return(_candidate_locked); + retval = _candidate_locked; else if (strcmp("startup", db) == 0) - return(_startup_locked); - return 0; + retval = _startup_locked; + else + clicon_err(OE_DB, 0, "No such database: %s", db); + return retval; } /*! Check if db exists @@ -1445,7 +1474,7 @@ kv_exists(xmldb_handle xh, char *filename = NULL; struct stat sb; - if (db2file(kh, db, &filename) < 0) + if (kv_db2file(kh, db, &filename) < 0) goto done; if (lstat(filename, &sb) < 0) retval = 0; @@ -1471,7 +1500,7 @@ kv_delete(xmldb_handle xh, struct kv_handle *kh = handle(xh); char *filename = NULL; - if (db2file(kh, db, &filename) < 0) + if (kv_db2file(kh, db, &filename) < 0) goto done; if (db_delete(filename) < 0) goto done; @@ -1496,7 +1525,7 @@ kv_init(xmldb_handle xh, struct kv_handle *kh = handle(xh); char *filename = NULL; - if (db2file(kh, db, &filename) < 0) + if (kv_db2file(kh, db, &filename) < 0) goto done; if (db_init(filename) < 0) goto done; diff --git a/datastore/keyvalue/clixon_keyvalue.h b/datastore/keyvalue/clixon_keyvalue.h index 85e6d8ab..88ae2621 100644 --- a/datastore/keyvalue/clixon_keyvalue.h +++ b/datastore/keyvalue/clixon_keyvalue.h @@ -46,7 +46,7 @@ int kv_put(xmldb_handle h, char *db, enum operation_type op, int kv_dump(FILE *f, char *dbfilename, char *rxkey); int kv_copy(xmldb_handle h, char *from, char *to); int kv_lock(xmldb_handle h, char *db, int pid); -int kv_unlock(xmldb_handle h, char *db, int pid); +int kv_unlock(xmldb_handle h, char *db); int kv_unlock_all(xmldb_handle h, int pid); int kv_islocked(xmldb_handle h, char *db); int kv_exists(xmldb_handle h, char *db); diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index f9c67fd7..4b6b3b91 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -106,7 +106,7 @@ static int _startup_locked = 0; * The filename reside in CLICON_XMLDB_DIR option */ static int -db2file(struct text_handle *th, +text_db2file(struct text_handle *th, char *db, char **filename) { @@ -126,7 +126,7 @@ db2file(struct text_handle *th, strcmp(db, "candidate") != 0 && strcmp(db, "startup") != 0 && strcmp(db, "tmp") != 0){ - clicon_err(OE_XML, 0, "Unexpected database: %s", db); + clicon_err(OE_XML, 0, "No such database: %s", db); goto done; } cprintf(cb, "%s/%s_db", dir, db); @@ -292,7 +292,7 @@ text_get(xmldb_handle xh, int i; struct text_handle *th = handle(xh); - if (db2file(th, db, &dbfile) < 0) + if (text_db2file(th, db, &dbfile) < 0) goto done; if (dbfile==NULL){ clicon_err(OE_XML, 0, "dbfile NULL"); @@ -348,7 +348,6 @@ text_get(xmldb_handle xh, if (fd != -1) close(fd); return retval; - } /*! Modify database provided an xml tree and an operation @@ -380,10 +379,37 @@ text_put(xmldb_handle xh, cxobj *xt) { int retval = -1; - // struct text_handle *th = handle(xh); + struct text_handle *th = handle(xh); + char *dbfile = NULL; + int fd = -1; + cbuf *cb = NULL; + if (text_db2file(th, db, &dbfile) < 0) + goto done; + if (dbfile==NULL){ + clicon_err(OE_XML, 0, "dbfile NULL"); + goto done; + } + if ((fd = open(dbfile, O_WRONLY | O_CREAT)) == -1) { + clicon_err(OE_UNIX, errno, "open(%s)", dbfile); + goto done; + } + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) + goto done; + if (write(fd, cbuf_get(cb), cbuf_len(cb)+1) < 0){ + clicon_err(OE_UNIX, errno, "write(%s)", dbfile); + goto done; + } retval = 0; - // done: + done: + if (fd != -1) + close(fd); + if (cb) + cbuf_free(cb); return retval; } @@ -441,8 +467,7 @@ text_lock(xmldb_handle xh, */ int text_unlock(xmldb_handle xh, - char *db, - int pid) + char *db) { // struct text_handle *th = handle(xh); @@ -515,7 +540,7 @@ text_exists(xmldb_handle xh, char *filename = NULL; struct stat sb; - if (db2file(th, db, &filename) < 0) + if (text_db2file(th, db, &filename) < 0) goto done; if (lstat(filename, &sb) < 0) retval = 0; diff --git a/datastore/text/clixon_xmldb_text.h b/datastore/text/clixon_xmldb_text.h index 83f5db43..c82765bc 100644 --- a/datastore/text/clixon_xmldb_text.h +++ b/datastore/text/clixon_xmldb_text.h @@ -46,7 +46,7 @@ int text_put(xmldb_handle h, char *db, enum operation_type op, int text_dump(FILE *f, char *dbfilename, char *rxkey); int text_copy(xmldb_handle h, char *from, char *to); int text_lock(xmldb_handle h, char *db, int pid); -int text_unlock(xmldb_handle h, char *db, int pid); +int text_unlock(xmldb_handle h, char *db); int text_unlock_all(xmldb_handle h, int pid); int text_islocked(xmldb_handle h, char *db); int text_exists(xmldb_handle h, char *db); diff --git a/lib/clixon/clixon_xml_db.h b/lib/clixon/clixon_xml_db.h index 3cad3679..eced86bf 100644 --- a/lib/clixon/clixon_xml_db.h +++ b/lib/clixon/clixon_xml_db.h @@ -89,7 +89,7 @@ typedef int (xmldb_copy_t)(xmldb_handle xh, char *from, char *to); typedef int (xmldb_lock_t)(xmldb_handle xh, char *db, int pid); /* Type of xmldb unlock function */ -typedef int (xmldb_unlock_t)(xmldb_handle xh, char *db, int pid); +typedef int (xmldb_unlock_t)(xmldb_handle xh, char *db); /* Type of xmldb unlock_all function */ typedef int (xmldb_unlock_all_t)(xmldb_handle xh, int pid); @@ -145,7 +145,7 @@ int xmldb_put(clicon_handle h, char *db, enum operation_type op, char *api_path, cxobj *xt); int xmldb_copy(clicon_handle h, char *from, char *to); int xmldb_lock(clicon_handle h, char *db, int pid); -int xmldb_unlock(clicon_handle h, char *db, int pid); +int xmldb_unlock(clicon_handle h, char *db); int xmldb_unlock_all(clicon_handle h, int pid); int xmldb_islocked(clicon_handle h, char *db); int xmldb_exists(clicon_handle h, char *db); diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index dcc5f0ee..5f816be2 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -68,5 +68,6 @@ int xml_tree_prune_unmarked(cxobj *xt, int *upmark); int xml_default(cxobj *x, void *arg); int xml_order(cxobj *x, void *arg); int xml_sanity(cxobj *x, void *arg); +int xml_apipath2xpath(yang_spec *yspec, cvec *pcvec, int pi, cbuf *path); #endif /* _CLIXON_XML_MAP_H_ */ diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 55109dbb..7d1ec8bd 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -843,7 +843,7 @@ clicon_xml2file(FILE *f, int level, int prettyprint) { - cbuf *cb; + cbuf *cb = NULL; int retval = -1; if ((cb = cbuf_new()) == NULL){ diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 8abfd12b..82a5b1ed 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -30,8 +30,8 @@ the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** - */ + */ #include #include @@ -169,7 +169,6 @@ xmldb_plugin_unload(clicon_handle h) * datastore. Note also that the xmldb handle is hidden in the clicon * handle, the clixon user does not need to handle it. Note also that * typically only the backend invokes the datastore. - * XXX what args does connect have? */ int xmldb_connect(clicon_handle h) @@ -201,8 +200,8 @@ xmldb_connect(clicon_handle h) int xmldb_disconnect(clicon_handle h) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -237,8 +236,8 @@ xmldb_getopt(clicon_handle h, char *optname, void **value) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -270,8 +269,8 @@ xmldb_setopt(clicon_handle h, char *optname, void *value) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -320,7 +319,6 @@ xmldb_setopt(clicon_handle h, * @see xpath_vec * @see xmldb_get */ - int xmldb_get(clicon_handle h, char *db, @@ -329,8 +327,8 @@ xmldb_get(clicon_handle h, cxobj ***xvec, size_t *xlen) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -345,8 +343,16 @@ xmldb_get(clicon_handle h, clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - clicon_log(LOG_WARNING, "%s: db:%s xpath:%s", __FUNCTION__, db, xpath); retval = xa->xa_get_fn(xh, db, xpath, xtop, xvec, xlen); +#if 0 /* XXX DEBUG */ + if (retval == 0) { + cbuf *cb = cbuf_new(); + clicon_xml2cbuf(cb, *xtop, 0, 0); + clicon_log(LOG_WARNING, "%s: db:%s xpath:%s xml:%s", + __FUNCTION__, db, xpath, cbuf_get(cb)); + cbuf_free(cb); + } +#endif done: return retval; } @@ -379,8 +385,8 @@ xmldb_put(clicon_handle h, char *api_path, cxobj *xt) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -395,6 +401,7 @@ xmldb_put(clicon_handle h, clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } +#if 0 /* XXX DEBUG */ { cbuf *cb = cbuf_new(); if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) @@ -404,6 +411,7 @@ xmldb_put(clicon_handle h, db, op, api_path, cbuf_get(cb)); cbuf_free(cb); } +#endif retval = xa->xa_put_fn(xh, db, op, api_path, xt); done: return retval; @@ -421,8 +429,8 @@ xmldb_copy(clicon_handle h, char *from, char *to) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -454,8 +462,8 @@ xmldb_lock(clicon_handle h, char *db, int pid) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -485,11 +493,10 @@ xmldb_lock(clicon_handle h, */ int xmldb_unlock(clicon_handle h, - char *db, - int pid) + char *db) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -504,7 +511,7 @@ xmldb_unlock(clicon_handle h, clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = xa->xa_unlock_fn(xh, db, pid); + retval = xa->xa_unlock_fn(xh, db); done: return retval; } @@ -519,8 +526,8 @@ int xmldb_unlock_all(clicon_handle h, int pid) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -551,8 +558,8 @@ int xmldb_islocked(clicon_handle h, char *db) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -583,8 +590,8 @@ int xmldb_exists(clicon_handle h, char *db) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -614,8 +621,8 @@ int xmldb_delete(clicon_handle h, char *db) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ @@ -645,8 +652,8 @@ int xmldb_init(clicon_handle h, char *db) { - int retval = -1; - xmldb_handle xh; + int retval = -1; + xmldb_handle xh; struct xmldb_api *xa; if ((xa = clicon_xmldb_api_get(h)) == NULL){ diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 9ac928a6..2d460f55 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1248,3 +1248,85 @@ xml_sanity(cxobj *xt, return retval; } +/*! Translate from restconf api-path to xml xpath + * eg a/b=c -> a/[b=c] + * @param[in] yspec Yang spec + * @param[in] pcvec api-path as cvec + * @param[in] pi Length of cvec + * @param[out] path The xpath as cligen bif variable string + */ +int +xml_apipath2xpath(yang_spec *yspec, + cvec *pcvec, + int pi, + cbuf *path) +{ + int retval = -1; + int i; + cg_var *cv; + char *name; + cvec *cvk = NULL; /* vector of index keys */ + yang_stmt *y = NULL; + char *val; + char *v; + yang_stmt *ykey; + cg_var *cvi; + + for (i=pi; iys_argument); + goto done; + } + clicon_debug(1, "ykey:%s", ykey->ys_argument); + + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + cvi = NULL; + /* Iterate over individual yang keys */ + cprintf(path, "/%s", name); + v = val; + while ((cvi = cvec_each(cvk, cvi)) != NULL){ + cprintf(path, "[%s=%s]", cv_string_get(cvi), v); + v += strlen(v)+1; + } + if (val) + free(val); + } + else{ + cprintf(path, "%s%s", (i==pi?"":"/"), name); + } + } + retval = 0; + done: + return retval; +} From d02015f4563ce3e654dbc8b84deadec5d77f2a90 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 17 Apr 2017 19:47:32 +0200 Subject: [PATCH 10/24] datastore text works with replace --- apps/restconf/restconf_lib.c | 90 ------- apps/restconf/restconf_lib.h | 1 - apps/restconf/restconf_methods.c | 2 +- datastore/datastore_client.c | 4 +- datastore/keyvalue/clixon_keyvalue.c | 69 ++++-- datastore/text/clixon_xmldb_text.c | 340 +++++++++++++++++++++++++-- lib/clixon/clixon_string.h | 5 +- lib/clixon/clixon_xml_map.h | 3 +- lib/src/clixon_string.c | 95 ++++++++ lib/src/clixon_xml.c | 6 +- lib/src/clixon_xml_db.c | 2 +- lib/src/clixon_xml_map.c | 96 ++++++-- test/lib.sh | 9 +- test/test4.sh | 2 +- test/test5.sh | 63 +++++ 15 files changed, 627 insertions(+), 160 deletions(-) create mode 100755 test/test5.sh diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 7f5f6e4d..150c50c7 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -110,96 +110,6 @@ clicon_debug_xml(int dbglevel, return retval; } -/*! Split a string into a cligen variable vector using 1st and 2nd delimiter - * Split a string first into elements delimited by delim1, then into - * pairs delimited by delim2. - * @param[in] string String to split - * @param[in] delim1 First delimiter char that delimits between elements - * @param[in] delim2 Second delimiter char for pairs within an element - * @param[out] cvp Created cligen variable vector, NOTE: can be NULL - * @retval 0 on OK - * @retval -1 error - * - * Example, assuming delim1 = '&' and delim2 = '=' - * a=b&c=d -> [[a,"b"][c="d"] - * kalle&c=d -> [[c="d"]] # Discard elements with no delim2 - * XXX differentiate between error and null cvec. - */ -int -str2cvec(char *string, - char delim1, - char delim2, - cvec **cvp) -{ - int retval = -1; - char *s; - char *s0 = NULL;; - char *val; /* value */ - char *valu; /* unescaped value */ - char *snext; /* next element in string */ - cvec *cvv = NULL; - cg_var *cv; - - clicon_debug(1, "%s %s", __FUNCTION__, string); - if ((s0 = strdup(string)) == NULL){ - clicon_debug(1, "error strdup %s", strerror(errno)); - goto err; - } - s = s0; - if ((cvv = cvec_new(0)) ==NULL){ - clicon_debug(1, "error cvec_new %s", strerror(errno)); - goto err; - } - while (s != NULL) { - /* - * In the pointer algorithm below: - * name1=val1; name2=val2; - * ^ ^ ^ - * | | | - * s val snext - */ - if ((snext = index(s, delim1)) != NULL) - *(snext++) = '\0'; - if ((val = index(s, delim2)) != NULL){ - *(val++) = '\0'; - if (percent_decode(val, &valu) < 0) - goto err; - if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ - clicon_debug(1, "error cvec_add %s", strerror(errno)); - goto err; - } - while ((strlen(s) > 0) && isblank(*s)) - s++; - cv_name_set(cv, s); - cv_string_set(cv, valu); - free(valu); valu = NULL; - } - else{ - if (strlen(s)){ - if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ - clicon_debug(1, "error cvec_add %s", strerror(errno)); - goto err; - } - cv_name_set(cv, s); - cv_string_set(cv, ""); - } - } - s = snext; - } - retval = 0; - done: - *cvp = cvv; - if (s0) - free(s0); - return retval; - err: - if (cvv){ - cvec_free(cvv); - cvv = NULL; - } - goto done; -} - /*! * @param[in] r Fastcgi request handle */ diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 8eba97bb..f133b15f 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -46,7 +46,6 @@ int notfound(FCGX_Request *r); int badrequest(FCGX_Request *r); int clicon_debug_xml(int dbglevel, char *str, cxobj *cx); -int str2cvec(char *string, char delim1, char delim2, cvec **cvp); int test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index f3e61858..b96b72b6 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -162,7 +162,7 @@ api_data_get_gen(clicon_handle h, yspec = clicon_dbspec_yang(h); if ((path = cbuf_new()) == NULL) goto done; - if (xml_apipath2xpath(yspec, pcvec, pi, path) < 0){ + if (api_path2xpath_cvv(yspec, pcvec, pi, path) < 0){ notfound(r); goto done; } diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c index 2a094e7b..9ecdf378 100644 --- a/datastore/datastore_client.c +++ b/datastore/datastore_client.c @@ -39,7 +39,6 @@ sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src sudo ./datastore_client -d candidate -b /usr/local/var/routing -p /home/olof/src/clixon/datastore/keyvalue/keyvalue.so -y /usr/local/share/routing/yang -m ietf-ip put merge / 'eth0true' - * */ #ifdef HAVE_CONFIG_H @@ -217,7 +216,8 @@ main(int argc, char **argv) usage(argv0); if (xmldb_get(h, db, argv[1], &xt, NULL, 0) < 0) goto done; - clicon_xml2file(stdout, xt, 0, 1); + clicon_xml2file(stdout, xt, 0, 0); + fprintf(stdout, "\n"); } else if (strcmp(cmd, "put")==0){ if (argc != 4) diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 65411349..8f73edd4 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -808,7 +808,6 @@ xmldb_put_xkey(struct kv_handle *kh, { int retval = -1; - cxobj *x = NULL; yang_stmt *y = NULL; yang_stmt *ykey; char **vec = NULL; @@ -839,7 +838,7 @@ xmldb_put_xkey(struct kv_handle *kh, if (kv_db2file(kh, db, &filename) < 0) goto done; if (xk == NULL || *xk!='/'){ - clicon_err(OE_DB, 0, "Invalid key: %s", xk); + clicon_err(OE_DB, 0, "Invalid api_path: %s", xk); goto done; } if ((ckey = cbuf_new()) == NULL){ @@ -872,9 +871,8 @@ xmldb_put_xkey(struct kv_handle *kh, cprintf(ckey, "/"); break; } - else - if ((y = yang_find_topnode(yspec, name)) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", x?xml_name(x):""); + else if ((y = yang_find_topnode(yspec, name)) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", name); goto done; } } @@ -916,7 +914,6 @@ xmldb_put_xkey(struct kv_handle *kh, free(valvec); if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) goto done; - if (cvec_len(cvk) != nvalvec){ clicon_err(OE_XML, errno, "List %s key length mismatch", name); goto done; @@ -931,7 +928,6 @@ xmldb_put_xkey(struct kv_handle *kh, else cprintf(ckey, "="); val2 = valvec[j++]; - cprintf(ckey, "%s", val2); cbuf_reset(csubkey); cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); @@ -975,7 +971,7 @@ xmldb_put_xkey(struct kv_handle *kh, if ((exists = db_exists(filename, xk)) < 0) goto done; if (exists == 0){ - clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk); + clicon_err(OE_DB, 0, "OP_DELETE: %s does not exist in database", xk); goto done; } case OP_REMOVE: @@ -1039,7 +1035,7 @@ static int xmldb_put_restconf_api_path(struct kv_handle *kh, char *db, enum operation_type op, - char *api_path, + char *xk, cxobj *xt) { int retval = -1; @@ -1047,6 +1043,12 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, yang_stmt *ykey; char **vec = NULL; int nvec; +#if 0 + char **valvec = NULL; + int nvalvec; + int j; + char *restval; +#endif int i; char *name; cg_var *cvi; @@ -1071,8 +1073,8 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, } if (kv_db2file(kh, db, &filename) < 0) goto done; - if (api_path == NULL || *api_path!='/'){ - clicon_err(OE_DB, 0, "Invalid api path: %s", api_path); + if (xk == NULL || *xk!='/'){ + clicon_err(OE_DB, 0, "Invalid api path: %s", xk); goto done; } if ((ckey = cbuf_new()) == NULL){ @@ -1083,13 +1085,13 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } - if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL) + if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) goto done; /* Remove trailing '/'. Like in /a/ -> /a */ if (nvec > 1 && !strlen(vec[nvec-1])) nvec--; if (nvec < 2){ - clicon_err(OE_XML, 0, "Malformed key: %s", api_path); + clicon_err(OE_XML, 0, "Malformed key: %s", xk); goto done; } i = 1; @@ -1099,14 +1101,19 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, *keys = '\0'; keys++; } +#if 0 + if ((restval = index(name, '=')) != NULL){ + *restval = '\0'; + restval++; + } +#endif if (i==1){ - if (!strlen(name) && (op==OP_DELETE || op == OP_REMOVE)){ + if (strlen(name)==0 && (op==OP_DELETE || op == OP_REMOVE)){ /* Special handling of "/" */ - cprintf(ckey, "/%s", name); + cprintf(ckey, "/"); break; } - else - if ((y = yang_find_topnode(yspec, name)) == NULL){ + else if ((y = yang_find_topnode(yspec, name)) == NULL){ clicon_err(OE_UNIX, errno, "No yang node found: %s", name); goto done; } @@ -1139,10 +1146,33 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, /* The value is a list of keys: [ ]* */ if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) goto done; +#if 0 + if (restval==NULL){ + clicon_err(OE_XML, 0, "malformed key, expected '='"); + goto done; + } + if (valvec) + free(valvec); + if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) + goto done; + if (cvec_len(cvk) != nvalvec){ + clicon_err(OE_XML, errno, "List %s key length mismatch", name); + goto done; + } + j = 0; +#endif cvi = NULL; /* Iterate over individual yang keys */ while ((cvi = cvec_each(cvk, cvi)) != NULL) { keyname = cv_string_get(cvi); +#if 0 + if (j) + cprintf(ckey, ","); + else + cprintf(ckey, "="); + val2 = valvec[j++]; + cprintf(ckey, "%s", val2); +#else // val2 = vec[i++]; /* No */ val2 = keys; if (i>nvec){ /* XXX >= ? */ @@ -1150,6 +1180,7 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, goto done; } cprintf(ckey, "=%s", val2); +#endif cbuf_reset(csubkey); cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname); if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE) @@ -1233,6 +1264,10 @@ xmldb_put_restconf_api_path(struct kv_handle *kh, cvec_free(cvk); if (vec) free(vec); +#if 0 + if (valvec) + free(valvec); +#endif unchunk_group(__FUNCTION__); return retval; } diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 4b6b3b91..4ee296b8 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -306,10 +306,23 @@ text_get(xmldb_handle xh, clicon_err(OE_UNIX, errno, "open(%s)", dbfile); goto done; } - if ((clicon_xml_parse_file(fd, &xt, "")) < 0) + /* Parse file into XML tree */ + if ((clicon_xml_parse_file(fd, &xt, "")) < 0) goto done; + /* Always assert a top-level called "config". + To ensure that, deal with two cases: + 1. File is empty -> rename top-level to "config" */ + if (xml_child_nr(xt) == 0){ + if (xml_name_set(xt, "config") < 0) + goto done; + } + /* 2. File is not empty ... -> replace root */ + else{ + assert(xml_child_nr(xt)==1); + if (xml_rootchild(xt, 0, &xt) < 0) + goto done; + } /* XXX Maybe the below is general function and should be moved to xmldb? */ - if (xpath_vec(xt, xpath?xpath:"/", &xvec, &xlen) < 0) goto done; @@ -332,13 +345,15 @@ text_get(xmldb_handle xh, *xlen0 = xlen; xlen = 0; } - if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) - goto done; - /* XXX does not work for top-level */ - if (xml_apply(xt, CX_ELMNT, xml_order, NULL) < 0) - goto done; - if (xml_apply(xt, CX_ELMNT, xml_sanity, NULL) < 0) - goto done; + if (0){ /* No xml_spec(xt) */ + if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) + goto done; + /* XXX does not work for top-level */ + if (xml_apply(xt, CX_ELMNT, xml_order, NULL) < 0) + goto done; + if (xml_apply(xt, CX_ELMNT, xml_sanity, NULL) < 0) + goto done; + } if (debug>1) clicon_xml2file(stderr, xt, 0, 1); *xtop = xt; @@ -350,6 +365,182 @@ text_get(xmldb_handle xh, return retval; } +/*! Check if child with fullmatch exists + * param[in] cvk vector of index keys +*/ +static cxobj * +find_keys(cxobj *xt, + char *name, + cvec *cvk, + char **valvec) +{ + cxobj *xi = NULL; + int j; + char *keyname; + char *val; + cg_var *cvi; + char *body; + + while ((xi = xml_child_each(xt, xi, CX_ELMNT)) != NULL) + if (strcmp(xml_name(xi), name) == 0){ + j = 0; /* All keys must match. */ + cvi = NULL; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + val = valvec[j++]; + if ((body = xml_find_body(xi, keyname)) == NULL) + continue; + if (strcmp(body, val)) + continue; + } + return xi; + } + return NULL; +} + +/*! Create tree from api-path, ie fill in xml tree from the path + */ +static int +text_create_tree(char *xk, + cxobj *xt, + enum operation_type op, + yang_spec *yspec, + cxobj **xp) +{ + int retval = -1; + char **vec = NULL; + int nvec; + int i; + int j; + char *name; + char *restval; + yang_stmt *y = NULL; + yang_stmt *ykey; + cxobj *x = NULL; + cxobj *xn = NULL; /* new */ + cxobj *xb; /* body */ + cvec *cvk = NULL; /* vector of index keys */ + char **valvec = NULL; + int nvalvec; + cg_var *cvi; + char *keyname; + char *val2; + + x = xt; + if (xk == NULL || *xk!='/'){ + clicon_err(OE_DB, 0, "Invalid key: %s", xk); + goto done; + } + if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) + goto done; + /* Remove trailing '/'. Like in /a/ -> /a */ + if (nvec > 1 && !strlen(vec[nvec-1])) + nvec--; + if (nvec < 1){ + clicon_err(OE_XML, 0, "Malformed key: %s", xk); + goto done; + } + i = 1; + while (i name:x restval=1,2 */ + if ((restval = index(name, '=')) != NULL){ + *restval = '\0'; + restval++; + } + if (y == NULL) /* top-node */ + y = yang_find_topnode(yspec, name); + else + y = yang_find_syntax((yang_node*)y, name); + if (y == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", name); + goto done; + } + i++; + switch (y->ys_keyword){ + case Y_LEAF_LIST: + if (restval==NULL){ + clicon_err(OE_XML, 0, "malformed key, expected '='"); + goto done; + } + fprintf(stderr, "XXX create =%s\n", restval); + x = xn; + break; + case Y_LIST: + if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, y->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + if (restval==NULL){ + clicon_err(OE_XML, 0, "malformed key, expected '='"); + goto done; + } + if (valvec) + free(valvec); + if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL) + goto done; + + if (cvec_len(cvk) != nvalvec){ + clicon_err(OE_XML, errno, "List %s key length mismatch", name); + goto done; + } + cvi = NULL; + /* Check if exists, if not, create */ + if ((xn = find_keys(x, name, cvk, valvec)) == NULL){ + /* create them, bit not if delete op */ + if (op == OP_DELETE || op == OP_REMOVE){ + retval = 0; + goto done; + } + if ((xn = xml_new(name, x)) == NULL) + goto done; + xml_type_set(xn, CX_ELMNT); + x = xn; + j = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + val2 = valvec[j++]; + if ((xn = xml_new(keyname, x)) == NULL) + goto done; + xml_type_set(xn, CX_ELMNT); + if ((xb = xml_new(keyname, xn)) == NULL) + goto done; + xml_type_set(xb, CX_BODY); + if (xml_value_set(xb, val2) <0) + goto done; + } + } + if (cvk){ + cvec_free(cvk); + cvk = NULL; + } + break; + default: /* eg Y_CONTAINER */ + if ((xn = xml_find(x, name)) == NULL){ + /* Already removed */ + if (op == OP_DELETE || op == OP_REMOVE){ + retval = 0; + goto done; + } + if ((xn = xml_new(name, x)) == NULL) + goto done; + xml_type_set(xn, CX_ELMNT); + } + x = xn; + break; + } + + } + *xp = x; + retval = 0; + done: + return retval; +} + + /*! Modify database provided an xml tree and an operation * * @param[in] xh XMLDB handle @@ -376,30 +567,105 @@ text_put(xmldb_handle xh, char *db, enum operation_type op, char *api_path, - cxobj *xt) + cxobj *xadd) { int retval = -1; struct text_handle *th = handle(xh); char *dbfile = NULL; int fd = -1; cbuf *cb = NULL; + cbuf *xpcb = NULL; /* xpath cbuf */ + yang_spec *yspec; + cxobj *xt = NULL; + cxobj *x = NULL; + cxobj *xc; + cxobj *xcopy = NULL; + if (xadd == NULL || strcmp(xml_name(xadd),"config")){ + clicon_err(OE_XML, 0, "Misformed xml, should start with "); + goto done; + } if (text_db2file(th, db, &dbfile) < 0) goto done; if (dbfile==NULL){ clicon_err(OE_XML, 0, "dbfile NULL"); goto done; } - if ((fd = open(dbfile, O_WRONLY | O_CREAT)) == -1) { + if ((yspec = th->th_yangspec) == NULL){ + clicon_err(OE_YANG, ENOENT, "No yang spec"); + goto done; + } + if ((fd = open(dbfile, O_RDONLY)) < 0) { clicon_err(OE_UNIX, errno, "open(%s)", dbfile); goto done; } + /* Parse file into XML tree */ + if ((clicon_xml_parse_file(fd, &xt, "")) < 0) + goto done; + /* Always assert a top-level called "config". + To ensure that, deal with two cases: + 1. File is empty -> rename top-level to "config" */ + if (xml_child_nr(xt) == 0){ + if (xml_name_set(xt, "config") < 0) + goto done; + } + /* 2. File is not empty ... -> replace root */ + else{ + assert(xml_child_nr(xt)==1); + if (xml_rootchild(xt, 0, &xt) < 0) + goto done; + } + /* If xpath find first occurence or api-path (this is where we apply xml) */ + if (api_path){ + if (text_create_tree(api_path, xt, op, yspec, &x) < 0) + goto done; + } + else + x = xt; + assert(x); + /* Here point of where xt is applied to xt is 'x' */ + switch (op){ + case OP_CREATE: + if (x){ + clicon_err(OE_DB, 0, "OP_CREATE: already exists in database"); + goto done; + } + case OP_REPLACE: + while ((xc = xml_child_i(x, 0)) != NULL) + xml_purge(xc); + case OP_MERGE: + /* Loop thru xadd's children */ + xc = NULL; + while ((xc = xml_child_each(xadd, xc, CX_ELMNT)) != NULL){ + if ((xcopy = xml_new("new", x)) == NULL) + goto done; + xml_copy(xc, xcopy); + } + break; + case OP_DELETE: + if (x==NULL){ + clicon_err(OE_DB, 0, "OP_DELETE: does not exist in database"); + goto done; + } + case OP_REMOVE: + while ((xc = xml_child_i(x, 0)) != NULL) + xml_purge(xc); + break; + case OP_NONE: + break; + } if ((cb = cbuf_new()) == NULL){ clicon_err(OE_XML, errno, "cbuf_new"); goto done; } if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) goto done; + /* Reopen file in write mode */ + close(fd); + if ((fd = open(dbfile, O_WRONLY | O_TRUNC, S_IRWXU)) < 0) { + clicon_err(OE_UNIX, errno, "open(%s)", dbfile); + goto done; + } if (write(fd, cbuf_get(cb), cbuf_len(cb)+1) < 0){ clicon_err(OE_UNIX, errno, "write(%s)", dbfile); goto done; @@ -410,6 +676,10 @@ text_put(xmldb_handle xh, close(fd); if (cb) cbuf_free(cb); + if (xpcb) + cbuf_free(xpcb); + if (xt) + xml_free(xt); return retval; } @@ -426,10 +696,23 @@ text_copy(xmldb_handle xh, char *to) { int retval = -1; - // struct text_handle *th = handle(xh); + struct text_handle *th = handle(xh); + char *fromfile = NULL; + char *tofile = NULL; + /* XXX lock */ + if (text_db2file(th, from, &fromfile) < 0) + goto done; + if (text_db2file(th, to, &tofile) < 0) + goto done; + if (clicon_file_copy(fromfile, tofile) < 0) + goto done; retval = 0; - // done: + done: + if (fromfile) + free(fromfile); + if (tofile) + free(tofile); return retval; } @@ -563,10 +846,17 @@ text_delete(xmldb_handle xh, char *db) { int retval = -1; -// struct text_handle *th = handle(xh); + char *filename = NULL; + struct text_handle *th = handle(xh); + if (text_db2file(th, db, &filename) < 0) + goto done; + if (unlink(filename) < 0){ + clicon_err(OE_DB, errno, "unlink %s", filename); + goto done; + } retval = 0; - // done: + done: return retval; } @@ -580,11 +870,23 @@ int text_init(xmldb_handle xh, char *db) { - int retval = -1; - // struct text_handle *th = handle(xh); + int retval = -1; + struct text_handle *th = handle(xh); + char *filename = NULL; + int fd = -1; - retval = 0; - // done: + if (text_db2file(th, db, &filename) < 0) + goto done; + if ((fd = open(filename, O_CREAT|O_WRONLY, S_IRWXU)) == -1) { + clicon_err(OE_UNIX, errno, "open(%s)", filename); + goto done; + } + retval = 0; + done: + if (filename) + free(filename); + if (fd != -1) + close(fd); return retval; } diff --git a/lib/clixon/clixon_string.h b/lib/clixon/clixon_string.h index d7fd4274..e0c9f1b7 100644 --- a/lib/clixon/clixon_string.h +++ b/lib/clixon/clixon_string.h @@ -56,10 +56,11 @@ static inline char * strdup4(char *str) */ char **clicon_strsep(char *string, char *delim, int *nvec0); char *clicon_strjoin (int argc, char **argv, char *delim); +int str2cvec(char *string, char delim1, char delim2, cvec **cvp); +int percent_encode(char *str, char **escp); +int percent_decode(char *esc, char **str); #ifndef HAVE_STRNDUP char *clicon_strndup (const char *, size_t); #endif /* ! HAVE_STRNDUP */ -int percent_encode(char *str, char **escp); -int percent_decode(char *esc, char **str); #endif /* _CLIXON_STRING_H_ */ diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 5f816be2..1cabeff6 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -68,6 +68,7 @@ int xml_tree_prune_unmarked(cxobj *xt, int *upmark); int xml_default(cxobj *x, void *arg); int xml_order(cxobj *x, void *arg); int xml_sanity(cxobj *x, void *arg); -int xml_apipath2xpath(yang_spec *yspec, cvec *pcvec, int pi, cbuf *path); +int api_path2xpath_cvv(yang_spec *yspec, cvec *cvv, int offset, cbuf *xpath); +int api_path2xpath(yang_spec *yspec, char *api_path, cbuf *xpath); #endif /* _CLIXON_XML_MAP_H_ */ diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index 98de980f..06b9fc49 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -47,6 +47,8 @@ #include #include +#include + /* clicon */ #include "clixon_queue.h" #include "clixon_string.h" @@ -241,6 +243,97 @@ percent_decode(char *esc, return retval; } +/*! Split a string into a cligen variable vector using 1st and 2nd delimiter + * Split a string first into elements delimited by delim1, then into + * pairs delimited by delim2. + * @param[in] string String to split + * @param[in] delim1 First delimiter char that delimits between elements + * @param[in] delim2 Second delimiter char for pairs within an element + * @param[out] cvp Created cligen variable vector, deallocate w cvec_free + * @retval 0 on OK + * @retval -1 error + * + * @example, + * Assuming delim1 = '&' and delim2 = '=' + * a=b&c=d -> [[a,"b"][c="d"] + * kalle&c=d -> [[c="d"]] # Discard elements with no delim2 + * XXX differentiate between error and null cvec. + */ +int +str2cvec(char *string, + char delim1, + char delim2, + cvec **cvp) +{ + int retval = -1; + char *s; + char *s0 = NULL;; + char *val; /* value */ + char *valu; /* unescaped value */ + char *snext; /* next element in string */ + cvec *cvv = NULL; + cg_var *cv; + + if ((s0 = strdup(string)) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto err; + } + s = s0; + if ((cvv = cvec_new(0)) ==NULL){ + clicon_err(OE_UNIX, errno, "cvec_new"); + goto err; + } + while (s != NULL) { + /* + * In the pointer algorithm below: + * name1=val1; name2=val2; + * ^ ^ ^ + * | | | + * s val snext + */ + if ((snext = index(s, delim1)) != NULL) + *(snext++) = '\0'; + if ((val = index(s, delim2)) != NULL){ + *(val++) = '\0'; + if (percent_decode(val, &valu) < 0) + goto err; + if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_add"); + goto err; + } + while ((strlen(s) > 0) && isblank(*s)) + s++; + cv_name_set(cv, s); + cv_string_set(cv, valu); + free(valu); valu = NULL; + } + else{ + if (strlen(s)){ + if ((cv = cvec_add(cvv, CGV_STRING)) == NULL){ + clicon_err(OE_UNIX, errno, "cvec_add"); + goto err; + } + cv_name_set(cv, s); + cv_string_set(cv, ""); + } + } + s = snext; + } + retval = 0; + done: + *cvp = cvv; + if (s0) + free(s0); + return retval; + err: + if (cvv){ + cvec_free(cvv); + cvv = NULL; + } + goto done; +} + + /*! strndup() for systems without it, such as xBSD */ #ifndef HAVE_STRNDUP @@ -265,6 +358,8 @@ clicon_strndup (const char *str, } #endif /* ! HAVE_STRNDUP */ + + /* * Turn this on for uni-test programs * Usage: clixon_string join diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 7d1ec8bd..e474cd4f 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -843,8 +843,8 @@ clicon_xml2file(FILE *f, int level, int prettyprint) { - cbuf *cb = NULL; int retval = -1; + cbuf *cb = NULL; if ((cb = cbuf_new()) == NULL){ clicon_err(OE_XML, errno, "cbuf_new"); @@ -1192,7 +1192,7 @@ copy_one(cxobj *xn0, * x1 should be a created placeholder. If x1 is non-empty, * the copied tree is appended to the existing tree. * @code - * x1 = xml_new("new", xc); + * x1 = xml_new("new", xparent); * xml_copy(x0, x1); * @endcode */ @@ -1200,7 +1200,7 @@ int xml_copy(cxobj *x0, cxobj *x1) { - int retval = -1; + int retval = -1; cxobj *x; cxobj *xcopy; diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 82a5b1ed..070f93fb 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -114,7 +114,7 @@ xmldb_plugin_load(clicon_handle h, /* Add API */ if (clicon_xmldb_api_set(h, xa) < 0) goto done; - clicon_log(LOG_WARNING, "xmldb plugin %s loaded", filename); + clicon_log(LOG_DEBUG, "xmldb plugin %s loaded", filename); retval = 0; done: if (retval < 0 && handle) diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 2d460f55..4fe808a8 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1125,7 +1125,7 @@ xml_tree_prune_unmarked(cxobj *xt, */ int xml_default(cxobj *xt, - void *arg) + void *arg) { int retval = -1; yang_stmt *ys; @@ -1135,7 +1135,11 @@ xml_default(cxobj *xt, cxobj *xb; char *str; - ys = (yang_stmt*)xml_spec(xt); + if ((ys = (yang_stmt*)xml_spec(xt)) == NULL){ + clicon_log(LOG_WARNING, "%s: no xml_spec(%s)", __FUNCTION__, xml_name(xt)); + retval = 0; + goto done; + } /* Check leaf defaults */ if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){ for (i=0; iys_len; i++){ @@ -1184,7 +1188,11 @@ xml_order(cxobj *xt, char *yname; /* yang child name */ char *xname; /* xml child name */ - y = (yang_stmt*)xml_spec(xt); + if ((y = (yang_stmt*)xml_spec(xt)) == NULL){ + clicon_log(LOG_WARNING, "%s: no xml_spec(%s)", __FUNCTION__, xml_name(xt)); + retval = 0; + goto done; + } j0 = 0; /* Go through xml children and ensure they are same order as yspec children */ for (i=0; iys_len; i++){ @@ -1217,7 +1225,7 @@ xml_order(cxobj *xt, } } retval = 0; - // done: + done: return retval; } @@ -1232,7 +1240,11 @@ xml_sanity(cxobj *xt, yang_stmt *ys; char *name; - ys = (yang_stmt*)xml_spec(xt); + if ((ys = (yang_stmt*)xml_spec(xt)) == NULL){ + clicon_log(LOG_WARNING, "%s: no xml_spec(%s)", __FUNCTION__, xml_name(xt)); + retval = 0; + goto done; + } name = xml_name(xt); if (ys==NULL){ clicon_err(OE_XML, 0, "No spec for xml node %s", name); @@ -1248,18 +1260,33 @@ xml_sanity(cxobj *xt, return retval; } -/*! Translate from restconf api-path to xml xpath +/*! Translate from restconf api-path in cvv form to xml xpath * eg a/b=c -> a/[b=c] * @param[in] yspec Yang spec * @param[in] pcvec api-path as cvec - * @param[in] pi Length of cvec - * @param[out] path The xpath as cligen bif variable string + * @param[in] pi Offset of cvec, where api-path starts + * @param[out] path The xpath as cbuf variable string, must be initializeed + * The api-path has some wierd encoding, use api_path2xpath() if you are + * confused. + * It works like this: + * Assume origin incoming path is + * "www.foo.com/restconf/a/b=c", pi is 2 and pcvec is: + * ["www.foo.com" "restconf" "a" "b=c"] + * which means the api-path is ["a" "b=c"] corresponding to "a/b=c" + * @code + * cbuf *xpath = cbuf_new(); + * if (api_path2xpath_cvv(yspec, cvv, i, xpath) + * err; + * ... access xpath as cbuf_get(xpath) + * cbuf_free(xpath) + * @endcode + * @see api_path2xpath for string api-path argument */ int -xml_apipath2xpath(yang_spec *yspec, - cvec *pcvec, - int pi, - cbuf *path) +api_path2xpath_cvv(yang_spec *yspec, + cvec *cvv, + int offset, + cbuf *xpath) { int retval = -1; int i; @@ -1272,12 +1299,12 @@ xml_apipath2xpath(yang_spec *yspec, yang_stmt *ykey; cg_var *cvi; - for (i=pi; i a/[b=c] + * @param[in] yspec Yang spec + * @param[in] str API-path as string + * @param[out] path The xpath as cbuf variable string, must be initializeed + * @code + * cbuf *xpath = cbuf_new(); + * if (api_path2xpath(yspec, "a/b=c", xpath) + * err; + * ... access xpath as cbuf_get(xpath) + * cbuf_free(xpath) + * @endcode + */ +int +api_path2xpath(yang_spec *yspec, + char *api_path, + cbuf *xpath) +{ + int retval = -1; + cvec *api_path_cvv = NULL; + + /* rest url eg /album=ricky/foo */ + if (str2cvec(api_path, '/', '=', &api_path_cvv) < 0) + goto done; + if (api_path2xpath_cvv(yspec, api_path_cvv, 0, xpath) < 0) + goto done; + retval = 0; + done: + if (api_path_cvv) + cvec_free(api_path_cvv); + return retval; +} diff --git a/test/lib.sh b/test/lib.sh index ba9672a4..83353f18 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -17,9 +17,8 @@ new(){ # sleep 1 } -# clicon_cli tester. First arg is command and second is expected outcome +# clixon tester. First arg is command and second is expected outcome expectfn(){ - cmd=$1 expect=$2 ret=`$cmd` @@ -40,7 +39,8 @@ expectfn(){ fi } -# clicon_cli tester. First arg is command and second is expected outcome +# clixon tester. First arg is command second is stdin and +# third is expected outcome expecteof(){ cmd=$1 input=$2 @@ -61,7 +61,8 @@ EOF fi } -# clicon_cli tester. First arg is command and second is expected outcome +# clixon tester. First arg is command second is stdin and +# third is expected outcome, fourth is how long to wait expectwait(){ cmd=$1 input=$2 diff --git a/test/test4.sh b/test/test4.sh index 1c472fbb..88fa31d6 100755 --- a/test/test4.sh +++ b/test/test4.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Test2: backend and netconf basic functionality +# Test4: Yang specifics: multi-keys and empty type # include err() and new() functions . ./lib.sh diff --git a/test/test5.sh b/test/test5.sh new file mode 100755 index 00000000..80137f01 --- /dev/null +++ b/test/test5.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Test5: datastore + +# include err() and new() functions +. ./lib.sh + +datastore=datastore_client + +cat < /tmp/ietf-ip.yang +module ietf-ip{ + container x { + list y { + key "a b"; + leaf a { + type string; + } + leaf b { + type string; + } + leaf c { + type string; + } + } + leaf d { + type empty; + } + } +} +EOF + +run(){ + name=$1 + dir=/tmp/$name + if [ ! -d $dir ]; then + mkdir $dir + fi + + rm -rf $dir/* + conf="-d candidate -b $dir -p ../datastore/$name/$name.so -y /tmp -m ietf-ip" + new "datastore $name init" + expectfn "$datastore $conf init" "" + + new "datastore $name get empty" + expectfn "$datastore $conf get /" "^$" + + new "datastore $name put top" + expectfn "$datastore $conf put replace / foobarfie" "" + + new "datastore $name get config" + expectfn "$datastore $conf get /" "^foobarfie$" + + new "datastore $name put delete" + expectfn "$datastore $conf put delete / " "" + + new "datastore $name get deleted" + expectfn "$datastore $conf get /" "^$" + + rm -rf $dir +} + +#run keyvalue # cant get the put to work +run text + From c2f52845f8bad54f03eba269d7357a2b80e71cef Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 23 Apr 2017 22:32:53 +0200 Subject: [PATCH 11/24] rm xml index; added xml util functions; further xmldb development --- datastore/datastore_client.c | 46 ++- datastore/keyvalue/clixon_keyvalue.c | 38 ++- datastore/text/clixon_xmldb_text.c | 450 +++++++++++++++++++++------ lib/clixon/clixon_xml.h | 5 +- lib/src/clixon_xml.c | 193 +++++++++--- lib/src/clixon_xml_db.c | 27 +- lib/src/clixon_xml_map.c | 6 +- lib/src/clixon_xml_parse.y | 15 +- lib/src/clixon_yang.c | 8 + test/test4.sh | 19 ++ test/test5.sh | 33 +- 11 files changed, 653 insertions(+), 187 deletions(-) diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c index 9ecdf378..01eb48b1 100644 --- a/datastore/datastore_client.c +++ b/datastore/datastore_client.c @@ -89,7 +89,7 @@ usage(char *argv0) "\t-m \tYang module. Mandatory\n" "and command is either:\n" "\tget \n" - "\tput (set|merge|delete) \n" + "\tput (merge|replace|create|delete|remove) \n" "\tcopy \n" "\tlock \n" "\tunlock\n" @@ -120,8 +120,7 @@ main(int argc, char **argv) int ret; int pid; enum operation_type op; - cxobj *xt; - cxobj *xn; + cxobj *xt = NULL; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, CLICON_LOG_STDERR); @@ -212,26 +211,31 @@ main(int argc, char **argv) if (xmldb_setopt(h, "yangspec", yspec) < 0) goto done; if (strcmp(cmd, "get")==0){ - if (argc != 2) + if (argc != 1 && argc != 2) usage(argv0); - if (xmldb_get(h, db, argv[1], &xt, NULL, 0) < 0) + if (xmldb_get(h, db, argc==2?argv[1]:"/", &xt, NULL, 0) < 0) goto done; clicon_xml2file(stdout, xt, 0, 0); + fprintf(stdout, "\n"); } else if (strcmp(cmd, "put")==0){ - if (argc != 4) + if (argc != 3 && argc != 4){ + clicon_err(OE_DB, 0, "Unexpected nr of args: %d", argc); usage(argv0); - if (xml_operation(argv[1], &op) < 0) + } + if (xml_operation(argv[1], &op) < 0){ + clicon_err(OE_DB, 0, "Unrecognized operation: %s", argv[1]); usage(argv0); - if (clicon_xml_parse_str(argv[3], &xt) < 0) + } + if (argc == 4){ + if (clicon_xml_parse_str(argv[3], &xt) < 0) + goto done; + if (xml_rootchild(xt, 0, &xt) < 0) + goto done; + } + if (xmldb_put(h, db, op, argv[2], xt) < 0) goto done; - if (xml_rootchild(xt, 0, &xn) < 0) - goto done; - if (xmldb_put(h, db, op, argv[2], xn) < 0) - goto done; - if (xn) - xml_free(xn); } else if (strcmp(cmd, "copy")==0){ if (argc != 2) @@ -285,9 +289,21 @@ main(int argc, char **argv) if (xmldb_init(h, db) < 0) goto done; } - else + else{ clicon_err(OE_DB, 0, "Unrecognized command: %s", cmd); + usage(argv0); + } + if (xmldb_disconnect(h) < 0) + goto done; + if (xmldb_plugin_unload(h) < 0) + goto done; done: + if (xt) + xml_free(xt); + if (h) + clicon_handle_exit(h); + if (yspec) + yspec_free(yspec); return 0; } diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 8f73edd4..1b4d3dc8 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -259,12 +259,13 @@ create_keyvalues(cxobj *x, char *keyname) { int retval = -1; + cxobj *xn; cxobj *xb; /* Check if key node exists */ - if ((xb = xml_new_spec(keyname, x, ykey)) == NULL) + if ((xn = xml_new_spec(keyname, x, ykey)) == NULL) goto done; - if ((xb = xml_new("body", xb)) == NULL) + if ((xb = xml_new("body", xn)) == NULL) goto done; xml_type_set(xb, CX_BODY); xml_value_set(xb, arg); @@ -831,6 +832,7 @@ xmldb_put_xkey(struct kv_handle *kh, yang_spec *yspec; char *filename = NULL; + // clicon_log(LOG_WARNING, "%s", __FUNCTION__); if ((yspec = kh->kh_yangspec) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; @@ -1322,24 +1324,28 @@ kv_put(xmldb_handle xh, if (db_init(dbfilename) < 0) goto done; } - while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ - /* An api path that is not / */ - if (api_path && strlen(api_path) && strcmp(api_path,"/")){ + if (api_path && strlen(api_path) && strcmp(api_path,"/")){ + // clicon_log(LOG_WARNING, "xmldb_put_restconf_api_path"); + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ if (xmldb_put_restconf_api_path(kh, db, op, api_path, x) < 0) goto done; - continue; } - if ((ys = yang_find_topnode(yspec, xml_name(x))) == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x)); - goto done; + } + else{ + // clicon_log(LOG_WARNING, "%s", __FUNCTION__); + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){ + if ((ys = yang_find_topnode(yspec, xml_name(x))) == NULL){ + clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x)); + goto done; + } + if (put(dbfilename, /* database name */ + x, /* xml root node */ + ys, /* yang statement of xml node */ + op, /* operation, eg merge/delete */ + "" /* aggregate xml key */ + ) < 0) + goto done; } - if (put(dbfilename, /* database name */ - x, /* xml root node */ - ys, /* yang statement of xml node */ - op, /* operation, eg merge/delete */ - "" /* aggregate xml key */ - ) < 0) - goto done; } retval = 0; done: diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 4ee296b8..a185ce75 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -339,12 +339,7 @@ text_get(xmldb_handle xh, goto done; if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0) goto done; - if (xvec0 && xlen0){ - *xvec0 = xvec; - xvec = NULL; - *xlen0 = xlen; - xlen = 0; - } + if (0){ /* No xml_spec(xt) */ if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) goto done; @@ -356,10 +351,22 @@ text_get(xmldb_handle xh, } if (debug>1) clicon_xml2file(stderr, xt, 0, 1); + if (xvec0 && xlen0){ + *xvec0 = xvec; + xvec = NULL; + *xlen0 = xlen; + xlen = 0; + } *xtop = xt; + xt = NULL; retval = 0; done: - // xml_free(xt); + if (xt) + xml_free(xt); + if (dbfile) + free(dbfile); + if (xvec) + free(xvec); if (fd != -1) close(fd); return retval; @@ -369,10 +376,10 @@ text_get(xmldb_handle xh, * param[in] cvk vector of index keys */ static cxobj * -find_keys(cxobj *xt, - char *name, - cvec *cvk, - char **valvec) +find_keys_vec(cxobj *xt, + char *name, + cvec *cvk, + char **valvec) { cxobj *xi = NULL; int j; @@ -383,29 +390,41 @@ find_keys(cxobj *xt, while ((xi = xml_child_each(xt, xi, CX_ELMNT)) != NULL) if (strcmp(xml_name(xi), name) == 0){ - j = 0; /* All keys must match. */ + j = 0; cvi = NULL; while ((cvi = cvec_each(cvk, cvi)) != NULL) { keyname = cv_string_get(cvi); val = valvec[j++]; if ((body = xml_find_body(xi, keyname)) == NULL) - continue; + break; if (strcmp(body, val)) - continue; + break; } - return xi; + /* All keys must match: loop terminates. */ + if (cvi==NULL) + return xi; } return NULL; } -/*! Create tree from api-path, ie fill in xml tree from the path +/*! Create 'modification' tree from api-path, ie fill in xml tree from the path + * @param[in] api_path api-path expression + * @param[in] xt XML tree. Find api-path (or create) in this tree + * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc + * @param[in] yspec Yang spec + * @param[out] xp Resulting xml tree corresponding to xt + * @param[out] xparp Parent of xp (xp can be NULL) + * @param[out] yp Yang spec matching xp + * @see xmldb_put_xkey for example */ static int -text_create_tree(char *xk, - cxobj *xt, - enum operation_type op, - yang_spec *yspec, - cxobj **xp) +text_create_modtree(char *api_path, + cxobj *xt, + enum operation_type op, + yang_spec *yspec, + cxobj **xp, + cxobj **xparp, + yang_node **yp) { int retval = -1; char **vec = NULL; @@ -417,6 +436,7 @@ text_create_tree(char *xk, yang_stmt *y = NULL; yang_stmt *ykey; cxobj *x = NULL; + cxobj *xpar = NULL; cxobj *xn = NULL; /* new */ cxobj *xb; /* body */ cvec *cvk = NULL; /* vector of index keys */ @@ -427,17 +447,18 @@ text_create_tree(char *xk, char *val2; x = xt; - if (xk == NULL || *xk!='/'){ - clicon_err(OE_DB, 0, "Invalid key: %s", xk); + xpar = xml_parent(xt); + if (api_path == NULL || *api_path!='/'){ + clicon_err(OE_DB, 0, "Invalid key: %s", api_path); goto done; } - if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL) + if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL) goto done; /* Remove trailing '/'. Like in /a/ -> /a */ if (nvec > 1 && !strlen(vec[nvec-1])) nvec--; if (nvec < 1){ - clicon_err(OE_XML, 0, "Malformed key: %s", xk); + clicon_err(OE_XML, 0, "Malformed key: %s", api_path); goto done; } i = 1; @@ -462,10 +483,42 @@ text_create_tree(char *xk, clicon_err(OE_XML, 0, "malformed key, expected '='"); goto done; } - fprintf(stderr, "XXX create =%s\n", restval); + /* See if it exists */ + xn = NULL; + while ((xn = xml_child_each(x, xn, CX_ELMNT)) != NULL) + if (strcmp(name, xml_name(xn)) == 0 && + strcmp(xml_body(xn),restval)==0) + break; + if (xn == NULL){ /* Not found, does not exist */ + switch (op){ + case OP_DELETE: /* not here, should be here */ + clicon_err(OE_XML, 0, "Object to delete does not exist"); + goto done; + break; + case OP_REMOVE: + goto ok; /* not here, no need to remove */ + break; + case OP_CREATE: + if (i==nvec) /* Last, dont create here */ + break; + default: + //XXX create_keyvalues(cxobj *x, + if ((xn = xml_new_spec(y->ys_argument, x, y)) == NULL) + goto done; + // xml_type_set(xn, CX_ELMNT); + if ((xb = xml_new("body", xn)) == NULL) + goto done; + xml_type_set(xb, CX_BODY); + if (xml_value_set(xb, restval) < 0) + goto done; + break; + } + } + xpar = x; x = xn; break; case Y_LIST: + /* Get the yang list key */ if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){ clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", __FUNCTION__, y->ys_argument); @@ -489,15 +542,23 @@ text_create_tree(char *xk, } cvi = NULL; /* Check if exists, if not, create */ - if ((xn = find_keys(x, name, cvk, valvec)) == NULL){ - /* create them, bit not if delete op */ - if (op == OP_DELETE || op == OP_REMOVE){ - retval = 0; + if ((xn = find_keys_vec(x, name, cvk, valvec)) == NULL){ + /* create them, but not if delete op */ + switch (op){ + case OP_DELETE: /* not here, should be here */ + clicon_err(OE_XML, 0, "Object to delete does not exist"); goto done; + break; + case OP_REMOVE: + goto ok; /* not here, no need to remove */ + break; + default: + if ((xn = xml_new(name, x)) == NULL) + goto done; + xml_type_set(xn, CX_ELMNT); + break; } - if ((xn = xml_new(name, x)) == NULL) - goto done; - xml_type_set(xn, CX_ELMNT); + xpar = x; x = xn; j = 0; while ((cvi = cvec_each(cvk, cvi)) != NULL) { @@ -506,50 +567,260 @@ text_create_tree(char *xk, if ((xn = xml_new(keyname, x)) == NULL) goto done; xml_type_set(xn, CX_ELMNT); - if ((xb = xml_new(keyname, xn)) == NULL) + if ((xb = xml_new("body", xn)) == NULL) goto done; xml_type_set(xb, CX_BODY); if (xml_value_set(xb, val2) <0) goto done; } } + else{ + xpar = x; + x = xn; + } if (cvk){ cvec_free(cvk); cvk = NULL; } break; - default: /* eg Y_CONTAINER */ - if ((xn = xml_find(x, name)) == NULL){ - /* Already removed */ - if (op == OP_DELETE || op == OP_REMOVE){ - retval = 0; - goto done; - } + default: /* eg Y_CONTAINER, Y_LEAF */ + if ((xn = xml_find(x, name)) == NULL){ + switch (op){ + case OP_DELETE: /* not here, should be here */ + clicon_err(OE_XML, 0, "Object to delete does not exist"); + goto done; + break; + case OP_REMOVE: + goto ok; /* not here, no need to remove */ + break; + case OP_CREATE: + if (i==nvec) /* Last, dont create here */ + break; + default: if ((xn = xml_new(name, x)) == NULL) goto done; xml_type_set(xn, CX_ELMNT); + break; } - x = xn; + } + else{ + if (op==OP_CREATE && i==nvec){ /* here, should not be here */ + clicon_err(OE_XML, 0, "Object to create already exists"); + goto done; + } + } + xpar = x; + x = xn; break; } - } *xp = x; + *xparp = xpar; + *yp = (yang_node*)y; + ok: + retval = 0; + done: + if (vec) + free(vec); + if (valvec) + free(valvec); + return retval; +} + +/*! Check if child with fullmatch exists + * param[in] cvk vector of index keys +*/ +static cxobj * +find_match(cxobj *x0, + cxobj *x1c, + yang_stmt *yc) +{ + cxobj *x0c = NULL; + char *keyname; + cvec *cvk = NULL; + cg_var *cvi; + char *b0; + char *b1; + yang_stmt *ykey; + char *name; + int ok; + + name = xml_name(x1c); + if (yc->ys_keyword != Y_LIST){ + x0c = xml_find(x0, name); + goto done; + } + if ((ykey = yang_find((yang_node*)yc, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, yc->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + x0c = NULL; + while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL) { + if (strcmp(xml_name(x0c), name)) + continue; + cvi = NULL; + ok = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + ok = 1; /* if we come here */ + if ((b0 = xml_find_body(x0c, keyname)) == NULL) + break; /* error case */ + if ((b1 = xml_find_body(x1c, keyname)) == NULL) + break; /* error case */ + if (strcmp(b0, b1)) + break; + ok = 2; /* and reaches here for all keynames, x0c is found. */ + } + if (ok == 2) + break; + } + done: + if (cvk) + cvec_free(cvk); + return x0c; +} + +/*! Modify a base tree x0 with x1 with yang spec y according to operation op + * @param[in] x0 Base xml tree + * @param[in] x1 xml tree which modifies base + * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc + * @param[in] y Yang spec corresponding to xml-node x0. NULL if no x0 + * Assume x0 and x1 are same on entry and that y is the spec + * @see put in clixon_keyvalue.c + * XXX: x1 är det som är under x0 + */ +static int +text_modify(cxobj *x0, + cxobj *x0p, + cxobj *x1, + enum operation_type op, + yang_node *y, + yang_spec *yspec) +{ + int retval = -1; + char *opstr; + char *name; + char *cname; /* child name */ + cxobj *x0c; /* base child */ + cxobj *x0b; /* base body */ + cxobj *x1c; /* mod child */ + char *x1bstr; /* mod body string */ + yang_stmt *yc; /* yang child */ + + clicon_debug(1, "%s %s", __FUNCTION__, x0?xml_name(x0):""); + /* Check for operations embedded in tree according to netconf */ + if (x1 && (opstr = xml_find_value(x1, "operation")) != NULL) + if (xml_operation(opstr, &op) < 0) + goto done; + if (x1 == NULL){ + switch(op){ + case OP_REPLACE: + if (x0) + xml_purge(x0); + case OP_CREATE: + case OP_MERGE: + break; + default: + break; + } + } + else { + assert(xml_type(x1) == CX_ELMNT); + name = xml_name(x1); + if (y && (y->yn_keyword == Y_LEAF_LIST || y->yn_keyword == Y_LEAF)){ + x1bstr = xml_body(x1); + switch(op){ + case OP_CREATE: + if (x0){ + clicon_err(OE_XML, 0, "Object to create already exists"); + goto done; + } + /* Fall thru */ + case OP_MERGE: + case OP_REPLACE: + if (x0==NULL){ + if ((x0 = xml_new_spec(name, x0p, y)) == NULL) + goto done; + if ((x0b = xml_new("body", x0)) == NULL) + goto done; + xml_type_set(x0b, CX_BODY); + } + if ((x0b = xml_body_get(x0)) == NULL) + goto done; + if (xml_value_set(x0b, x1bstr) < 0) + goto done; + break; + default: + break; + } /* switch op */ + } /* if LEAF|LEAF_LIST */ + else { /* eg Y_CONTAINER */ + switch(op){ + case OP_CREATE: + /* top-level object is a special case, ie when + * x0 parent is NULL + * or x1 is empty + */ + if ((x0p && x0) || + (x0p==NULL && xml_child_nr(x1) == 0)){ + clicon_err(OE_XML, 0, "Object to create already exists"); + goto done; + } + case OP_REPLACE: + /* top-level object is a special case, ie when + * x0 parent is NULL, + * or x1 is empty + */ + if ((x0p && x0) || + (x0p==NULL && xml_child_nr(x1) == 0)){ + xml_purge(x0); + x0 = NULL; + } + case OP_MERGE: + if (x0==NULL){ + if ((x0 = xml_new_spec(name, x0p, y)) == NULL) + goto done; + } + x1c = NULL; + while ((x1c = xml_child_each(x1, x1c, CX_ELMNT)) != NULL) { + cname = xml_name(x1c); + /* XXX This is more than find, if a list keys must match */ + + if (y == NULL) + yc = yang_find_topnode(yspec, cname); /* still NULL for config */ + else + yc = yang_find_syntax(y, cname); + x0c = find_match(x0, x1c, yc); + if (text_modify(x0c, x0, x1c, op, (yang_node*)yc, yspec) < 0) + goto done; + } + break; + default: + break; + } /* CONTAINER switch op */ + } /* else Y_CONTAINER */ + } /* x1 != NULL */ + // ok: retval = 0; done: return retval; } - /*! Modify database provided an xml tree and an operation * * @param[in] xh XMLDB handle * @param[in] db running or candidate - * @param[in] xt xml-tree. Top-level symbol is dummy * @param[in] op OP_MERGE: just add it. * OP_REPLACE: first delete whole database * OP_NONE: operation attribute in xml determines operation - * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13]) + * @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13 +]) + * @param[in] xadd xml-tree to merge/replace. Top-level symbol is 'config'. + * Should be empty or '' if delete? * @retval 0 OK * @retval -1 Error * The xml may contain the "operation" attribute which defines the operation. @@ -557,17 +828,16 @@ text_create_tree(char *xk, * cxobj *xt; * if (clicon_xml_parse_str("17", &xt) < 0) * err; - * if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0) + * if (xmldb_put(h, "running", OP_MERGE, "/", xt) < 0) * err; * @endcode - * @see xmldb_put_xkey for single key */ int text_put(xmldb_handle xh, - char *db, - enum operation_type op, - char *api_path, - cxobj *xadd) + char *db, + enum operation_type op, + char *api_path, + cxobj *xmod) { int retval = -1; struct text_handle *th = handle(xh); @@ -577,12 +847,14 @@ text_put(xmldb_handle xh, cbuf *xpcb = NULL; /* xpath cbuf */ yang_spec *yspec; cxobj *xt = NULL; - cxobj *x = NULL; + cxobj *xbase = NULL; + cxobj *xbasep = NULL; /* parent */ cxobj *xc; - cxobj *xcopy = NULL; + cxobj *xnew = NULL; + yang_node *y = NULL; - if (xadd == NULL || strcmp(xml_name(xadd),"config")){ - clicon_err(OE_XML, 0, "Misformed xml, should start with "); + if ((op==OP_DELETE || op==OP_REMOVE) && xmod){ + clicon_err(OE_XML, 0, "xml tree should be NULL for REMOVE/DELETE"); goto done; } if (text_db2file(th, db, &dbfile) < 0) @@ -615,45 +887,39 @@ text_put(xmldb_handle xh, if (xml_rootchild(xt, 0, &xt) < 0) goto done; } + /* here xt looks like: ... */ /* If xpath find first occurence or api-path (this is where we apply xml) */ if (api_path){ - if (text_create_tree(api_path, xt, op, yspec, &x) < 0) + if (text_create_modtree(api_path, xt, op, yspec, &xbase, &xbasep, &y) < 0) goto done; } - else - x = xt; - assert(x); - /* Here point of where xt is applied to xt is 'x' */ - switch (op){ - case OP_CREATE: - if (x){ - clicon_err(OE_DB, 0, "OP_CREATE: already exists in database"); - goto done; - } - case OP_REPLACE: - while ((xc = xml_child_i(x, 0)) != NULL) - xml_purge(xc); - case OP_MERGE: - /* Loop thru xadd's children */ - xc = NULL; - while ((xc = xml_child_each(xadd, xc, CX_ELMNT)) != NULL){ - if ((xcopy = xml_new("new", x)) == NULL) - goto done; - xml_copy(xc, xcopy); - } - break; - case OP_DELETE: - if (x==NULL){ - clicon_err(OE_DB, 0, "OP_DELETE: does not exist in database"); - goto done; - } - case OP_REMOVE: - while ((xc = xml_child_i(x, 0)) != NULL) - xml_purge(xc); - break; - case OP_NONE: - break; + else{ + xbase = xt; /* defer y since x points to config */ + xbasep = xml_parent(xt); /* NULL */ + assert(strcmp(xml_name(xbase),"config")==0); } + + /* + * Modify base tree x with modification xmod + */ + if (op == OP_DELETE || op == OP_REMOVE){ + /* special case if top-level, dont purge top-level */ + if (xt == xbase){ + xc = NULL; + while ((xc = xml_child_each(xt, xc, CX_ELMNT)) != NULL){ + xml_purge(xc); + xc = NULL; /* reset iterator */ + } + } + else + if (xbase) + xml_purge(xbase); + } + else + if (text_modify(xbase, xbasep, xmod, op, (yang_node*)y, yspec) < 0) + goto done; + // output: + /* Print out top-level xml tree after modification to file */ if ((cb = cbuf_new()) == NULL){ clicon_err(OE_XML, errno, "cbuf_new"); goto done; @@ -672,6 +938,8 @@ text_put(xmldb_handle xh, } retval = 0; done: + if (dbfile) + free(dbfile); if (fd != -1) close(fd); if (cb) @@ -680,6 +948,8 @@ text_put(xmldb_handle xh, cbuf_free(xpcb); if (xt) xml_free(xt); + if (xnew) + xml_free(xnew); return retval; } diff --git a/lib/clixon/clixon_xml.h b/lib/clixon/clixon_xml.h index 91da69e5..43ab57d9 100644 --- a/lib/clixon/clixon_xml.h +++ b/lib/clixon/clixon_xml.h @@ -71,6 +71,7 @@ typedef int (xml_applyfn_t)(cxobj *yn, void *arg); /* * Prototypes */ +char *xml_type2str(enum cxobj_type type); char *xml_name(cxobj *xn); int xml_name_set(cxobj *xn, char *name); char *xml_namespace(cxobj *xn); @@ -87,8 +88,6 @@ int xml_value_set(cxobj *xn, char *val); char *xml_value_append(cxobj *xn, char *val); enum cxobj_type xml_type(cxobj *xn); int xml_type_set(cxobj *xn, enum cxobj_type type); -int xml_index(cxobj *xn); -int xml_index_set(cxobj *xn, int index); cg_var *xml_cv_get(cxobj *xn); int xml_cv_set(cxobj *xn, cg_var *cv); @@ -113,6 +112,7 @@ int xml_rm(cxobj *xc); int xml_rootchild(cxobj *xp, int i, cxobj **xcp); char *xml_body(cxobj *xn); +cxobj *xml_body_get(cxobj *xn); char *xml_find_value(cxobj *xn_parent, char *name); char *xml_find_body(cxobj *xn, char *name); @@ -127,6 +127,7 @@ int clicon_xml_parse_file(int fd, cxobj **xml_top, char *endtag); int clicon_xml_parse_str(char *str, cxobj **xml_top); int clicon_xml_parse(cxobj **cxtop, char *format, ...); +int xmltree2cbuf(cbuf *cb, cxobj *x, int level); int xml_copy(cxobj *x0, cxobj *x1); cxobj *xml_dup(cxobj *x0); diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index e474cd4f..ce836af7 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -75,7 +75,6 @@ struct xml{ int x_childvec_len; /* length of vector */ enum cxobj_type x_type; /* type of node: element, attribute, body */ char *x_value; /* attribute and body nodes have values */ - int x_index; /* key node, cf sql index */ int _x_vector_i; /* internal use: xml_child_each */ int x_flags; /* Flags according to XML_FLAG_* above */ void *x_spec; /* Pointer to specification, eg yang, by @@ -83,6 +82,36 @@ struct xml{ cg_var *x_cv; /* If body this contains the typed value */ }; +/* Type to string conversion */ +struct map_str2int{ + char *ms_str; + enum cxobj_type ms_type; +}; + +/* Mapping between xml type <--> string */ +static const struct map_str2int xsmap[] = { + {"error", CX_ERROR}, + {"element", CX_ELMNT}, + {"attr", CX_ATTR}, + {"body", CX_BODY}, + {NULL, -1} +}; + +/*! Translate from xml type in enum form to string keyword + * @param[in] type Xml type + * @retval str String keyword + */ +char * +xml_type2str(enum cxobj_type type) +{ + const struct map_str2int *xs; + + for (xs = &xsmap[0]; xs->ms_str; xs++) + if (xs->ms_type == type) + return xs->ms_str; + return NULL; +} + /* * Access functions */ @@ -222,7 +251,7 @@ xml_value(cxobj *xn) /*! Set value of xml node, value is copied * @param[in] xn xml node - * @param[in] val new value, null-terminated string, copied by function + * @param[in] val new value, null-terminated string, copied by function * @retval -1 on error with clicon-err set * @retval 0 OK */ @@ -293,33 +322,6 @@ xml_type_set(cxobj *xn, return old; } -/*! Get index/key of xnode - * @param[in] xn xml node - * @retval index of xml node - * index/key is used in case of yang list constructs where one element is key - */ -int -xml_index(cxobj *xn) -{ - return xn->x_index; -} - -/*! Set index of xnode - * @param[in] xn xml node - * @param[in] index new index - * @retval index old index - * index/key is used in case of yang list constructs where one element is key - */ -int -xml_index_set(cxobj *xn, - int index) -{ - int old = xn->x_index; - - xn->x_index = index; - return old; -} - /*! Get cligen variable associated with node * @param[in] xn xml node * @retval cv Cligen variable if set @@ -757,6 +759,16 @@ xml_body(cxobj *xn) return NULL; } +cxobj * +xml_body_get(cxobj *xn) +{ + cxobj *xb = NULL; + + while ((xb = xml_child_each(xn, xb, CX_BODY)) != NULL) + return xb; + return NULL; +} + /*! Find and return the value of a sub xml node * * The value can be of an attribute or body. @@ -881,8 +893,8 @@ xml_print(FILE *f, /*! Print an XML tree structure to a cligen buffer * * @param[in,out] cb Cligen buffer to write to - * @param[in] xn clicon xml tree - * @param[in] level how many spaces to insert before each line + * @param[in] xn Clicon xml tree + * @param[in] level Indentation level * @param[in] prettyprint insert \n and spaces tomake the xml more readable. * * @code @@ -896,47 +908,50 @@ xml_print(FILE *f, */ int clicon_xml2cbuf(cbuf *cb, - cxobj *cx, + cxobj *x, int level, int prettyprint) { cxobj *xc; + char *name; - switch(xml_type(cx)){ + name = xml_name(x); + switch(xml_type(x)){ case CX_BODY: - cprintf(cb, "%s", xml_value(cx)); + cprintf(cb, "%s", xml_value(x)); break; case CX_ATTR: cprintf(cb, " "); - if (xml_namespace(cx)) - cprintf(cb, "%s:", xml_namespace(cx)); - cprintf(cb, "%s=\"%s\"", xml_name(cx), xml_value(cx)); + if (xml_namespace(x)) + cprintf(cb, "%s:", xml_namespace(x)); + cprintf(cb, "%s=\"%s\"", name, xml_value(x)); break; case CX_ELMNT: cprintf(cb, "%*s<", prettyprint?(level*XML_INDENT):0, ""); - if (xml_namespace(cx)) - cprintf(cb, "%s:", xml_namespace(cx)); - cprintf(cb, "%s", xml_name(cx)); + if (xml_namespace(x)) + cprintf(cb, "%s:", xml_namespace(x)); + cprintf(cb, "%s", name); xc = NULL; - while ((xc = xml_child_each(cx, xc, CX_ATTR)) != NULL) + /* print attributes only */ + while ((xc = xml_child_each(x, xc, CX_ATTR)) != NULL) clicon_xml2cbuf(cb, xc, level+1, prettyprint); /* Check for special case instead of */ - if (xml_body(cx)==NULL && xml_child_nr(cx)==0) + if (xml_body(x)==NULL && xml_child_nr(x)==0) cprintf(cb, "/>"); else{ cprintf(cb, ">"); - if (prettyprint && xml_body(cx)==NULL) + if (prettyprint && xml_body(x)==NULL) cprintf(cb, "\n"); xc = NULL; - while ((xc = xml_child_each(cx, xc, -1)) != NULL) { + while ((xc = xml_child_each(x, xc, -1)) != NULL) { if (xml_type(xc) == CX_ATTR) continue; else clicon_xml2cbuf(cb, xc, level+1, prettyprint); } - if (prettyprint && xml_body(cx)==NULL) + if (prettyprint && xml_body(x)==NULL) cprintf(cb, "%*s", level*XML_INDENT, ""); - cprintf(cb, "", xml_name(cx)); + cprintf(cb, "", name); } if (prettyprint) cprintf(cb, "\n"); @@ -975,6 +990,44 @@ xml_parse(char *str, return retval; } +/*! Print actual xml tree datastructures (not xml), mainly for debugging + * @param[in,out] cb Cligen buffer to write to + * @param[in] xn Clicon xml tree + * @param[in] level Indentation level + */ +int +xmltree2cbuf(cbuf *cb, + cxobj *x, + int level) +{ + cxobj *xc; + int i; + + for (i=0; ix_flags) + cprintf(cb, " flags:0x%x", x->x_flags); + if (xml_child_nr(x)) + cprintf(cb, " {"); + cprintf(cb, "\n"); + xc = NULL; + while ((xc = xml_child_each(x, xc, -1)) != NULL) + xmltree2cbuf(cb, xc, level+1); + if (xml_child_nr(x)){ + for (i=0; i" | xml +*/ +#if 0 /* Test program */ + +static int +usage(char *argv0) +{ + fprintf(stderr, "usage:%s.\n\tInput on stdin\n", argv0); + exit(0); +} + +int +main(int argc, char **argv) +{ + cxobj *xt; + cxobj *xc; + cbuf *cb = cbuf_new(); + + if (argc != 1){ + usage(argv[0]); + return 0; + } + if (clicon_xml_parse_file(0, &xt, "") < 0){ + fprintf(stderr, "parsing 2\n"); + return -1; + } + xc = NULL; + while ((xc = xml_child_each(xt, xc, -1)) != NULL) { + xmltree2cbuf(cb, xc, 0); /* dump data structures */ + //clicon_xml2cbuf(cb, xc, 0, 1); /* print xml */ + } + fprintf(stdout, "%s", cbuf_get(cb)); + if (xt) + xml_free(xt); + if (cb) + cbuf_free(cb); + return 0; +} + +#endif /* Test program */ + diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 070f93fb..0ebf7c8e 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -67,7 +67,7 @@ * If init function fails (not found, wrong version, etc) print a log and dont * add it. * @param[in] h CLicon handle - * @param[in] filename Actual filename with path + * @param[in] filename Actual filename including path */ int xmldb_plugin_load(clicon_handle h, @@ -125,7 +125,11 @@ xmldb_plugin_load(clicon_handle h, goto done; } -/*! Unload the xmldb storage plugin */ +/*! Unload the xmldb storage plugin + * @param[in] h Clicon handle + * @retval 0 OK + * @retval -1 Error + */ int xmldb_plugin_unload(clicon_handle h) { @@ -162,9 +166,10 @@ xmldb_plugin_unload(clicon_handle h) return retval; } -/*! Connect to a datastore plugin - * @retval handle Use this handle for other API calls - * @retval NULL Error +/*! Connect to a datastore plugin, allocate handle to be used in API calls + * @param[in] h Clicon handle + * @retval 0 OK + * @retval -1 Error * @note You can do several connects, and have multiple connections to the same * datastore. Note also that the xmldb handle is hidden in the clicon * handle, the clixon user does not need to handle it. Note also that @@ -196,7 +201,8 @@ xmldb_connect(clicon_handle h) /*! Disconnect from a datastore plugin and deallocate handle * @param[in] handle Disconect and deallocate from this handle * @retval 0 OK - */ + * @retval -1 Error + */ int xmldb_disconnect(clicon_handle h) { @@ -225,7 +231,7 @@ xmldb_disconnect(clicon_handle h) } /*! Get value of generic plugin option. Type of value is givenby context - * @param[in] xh XMLDB handle + * @param[in] h Clicon handle * @param[in] optname Option name * @param[out] value Pointer to Value of option * @retval 0 OK @@ -258,7 +264,7 @@ xmldb_getopt(clicon_handle h, } /*! Set value of generic plugin option. Type of value is givenby context - * @param[in] xh XMLDB handle + * @param[in] h Clicon handle * @param[in] optname Option name * @param[in] value Value of option * @retval 0 OK @@ -404,8 +410,9 @@ xmldb_put(clicon_handle h, #if 0 /* XXX DEBUG */ { cbuf *cb = cbuf_new(); - if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) - goto done; + if (xt) + if (clicon_xml2cbuf(cb, xt, 0, 0) < 0) + goto done; clicon_log(LOG_WARNING, "%s: db:%s op:%d api_path:%s xml:%s", __FUNCTION__, db, op, api_path, cbuf_get(cb)); diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 4fe808a8..c89cc4c6 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -241,7 +241,7 @@ xml2cli(FILE *f, !index GT_VARS T !index GT_ALL T */ - bool = !leaf(x) || gt == GT_ALL || (gt == GT_VARS && !xml_index(x)); + bool = !leaf(x) || gt == GT_ALL || (gt == GT_VARS); // bool = (!x->xn_index || gt == GT_ALL); if (bool){ if (cbuf_len(cbpre)) @@ -253,12 +253,12 @@ xml2cli(FILE *f, i = 0; while ((xe = xml_child_each(x, xe, -1)) != NULL){ /* Dont call this if it is index and there are other following */ - if (xml_index(xe) && i < nr-1) + if (0 && i < nr-1) ; else if (xml2cli(f, xe, cbuf_get(cbpre), gt) < 0) goto done; - if (xml_index(xe)){ /* assume index is first, otherwise need one more while */ + if (0){ /* assume index is first, otherwise need one more while */ if (gt == GT_ALL) cprintf(cbpre, " %s", xml_name(xe)); cprintf(cbpre, " %s", xml_value(xml_child_i(xe, 0))); diff --git a/lib/src/clixon_xml_parse.y b/lib/src/clixon_xml_parse.y index ede62b8e..113b518f 100644 --- a/lib/src/clixon_xml_parse.y +++ b/lib/src/clixon_xml_parse.y @@ -175,8 +175,10 @@ xml_parse_endslash_post(struct xml_parse_yacc_arg *ya) return 0; } +/*! Called at */ static int -xml_parse_bslash1(struct xml_parse_yacc_arg *ya, char *name) +xml_parse_bslash1(struct xml_parse_yacc_arg *ya, + char *name) { int retval = -1; cxobj *x = ya->ya_xelement; @@ -199,8 +201,10 @@ xml_parse_bslash1(struct xml_parse_yacc_arg *ya, char *name) ; else{ xc = NULL; - while ((xc = xml_child_each(x, xc, CX_BODY)) != NULL) - xml_value_set(xc, ""); /* XXX remove */ + while ((xc = xml_child_each(x, xc, CX_BODY)) != NULL) { + xml_purge(xc); + xc = NULL; /* reset iterator */ + } } } retval = 0; @@ -209,8 +213,11 @@ xml_parse_bslash1(struct xml_parse_yacc_arg *ya, char *name) return retval; } +/*! Called at */ static int -xml_parse_bslash2(struct xml_parse_yacc_arg *ya, char *namespace, char *name) +xml_parse_bslash2(struct xml_parse_yacc_arg *ya, + char *namespace, + char *name) { int retval = -1; cxobj *x = ya->ya_xelement; diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 14a03e84..14cead1f 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -160,6 +160,10 @@ static const struct map_str2int ykmap[] = { {NULL, -1} }; +/*! Create new yang specification + * @retval yspec Free with yspec_free() + * @retval NULL Error + */ yang_spec * yspec_new(void) { @@ -174,6 +178,10 @@ yspec_new(void) return yspec; } +/*! Create new yang node/statement + * @retval ys Free with ys_free() + * @retval NULL Error + */ yang_stmt * ys_new(enum rfc_6020 keyw) { diff --git a/test/test4.sh b/test/test4.sh index 88fa31d6..38875a41 100755 --- a/test/test4.sh +++ b/test/test4.sh @@ -26,6 +26,16 @@ module ietf-ip{ leaf d { type empty; } + container f { + leaf-list e { + type string; + } + } + container h { + leaf j { + type string; + } + } } } EOF @@ -52,6 +62,15 @@ expecteof "$clixon_netconf -qf $clixon_cf -y /tmp/test" "]]> new "netconf get config xpath" expecteof "$clixon_netconf -qf $clixon_cf -y /tmp/test" "]]>]]>" "^125]]>]]>$" +new "netconf edit leaf-list" +expecteof "$clixon_netconf -qf $clixon_cf -y /tmp/test" "hejhopp]]>]]>" "^]]>]]>$" + +new "netconf get leaf-list" +expecteof "$clixon_netconf -qf $clixon_cf -y /tmp/test" "]]>]]>" "^hejhopp]]>]]>$" + +new "netconf get leaf-list path" +expecteof "$clixon_netconf -qf $clixon_cf -y /tmp/test" "]]>]]>" "^hejhopp]]>]]>$" + new "Kill backend" # Check if still alive pid=`pgrep clixon_backend` diff --git a/test/test5.sh b/test/test5.sh index 80137f01..812b2e3f 100755 --- a/test/test5.sh +++ b/test/test5.sh @@ -6,6 +6,7 @@ datastore=datastore_client + cat < /tmp/ietf-ip.yang module ietf-ip{ container x { @@ -24,22 +25,52 @@ module ietf-ip{ leaf d { type empty; } + container f { + leaf-list e { + type string; + } + } + container h { + leaf j { + type string; + } + } } } EOF +db='12first-entry13second-entry23third-entryabcastring' + run(){ name=$1 dir=/tmp/$name if [ ! -d $dir ]; then mkdir $dir fi - rm -rf $dir/* conf="-d candidate -b $dir -p ../datastore/$name/$name.so -y /tmp -m ietf-ip" + echo "conf:$conf" new "datastore $name init" expectfn "$datastore $conf init" "" + new "datastore $name put top" + echo "$datastore $conf put replace '/' $db" + expectfn "$datastore $conf put replace '/' \"$db\"" "" + +return + new "datastore $name get" + expectfn "$datastore $conf get /" $db + + new "datastore $name put rm" + expectfn "$datastore $conf put remove /x/g" + + new "datastore $name put top" + expectfn "$datastore $conf put replace / $db" + + new "datastore $name put del" + expectfn "$datastore $conf put delete /x/g" + +return new "datastore $name get empty" expectfn "$datastore $conf get /" "^$" From 2f30bda7d4c00b2275c32397f760bc535beea4a4 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 24 Apr 2017 13:00:31 +0200 Subject: [PATCH 12/24] text datastore full tests --- datastore/text/clixon_xmldb_text.c | 126 +++++++++++++++++------------ test/test4.sh | 3 + test/test5.sh | 43 ++++++++-- 3 files changed, 114 insertions(+), 58 deletions(-) diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index a185ce75..3710d96b 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -418,7 +418,7 @@ find_keys_vec(cxobj *xt, * @see xmldb_put_xkey for example */ static int -text_create_modtree(char *api_path, +text_apipath_modify(char *api_path, cxobj *xt, enum operation_type op, yang_spec *yspec, @@ -473,7 +473,7 @@ text_create_modtree(char *api_path, else y = yang_find_syntax((yang_node*)y, name); if (y == NULL){ - clicon_err(OE_UNIX, errno, "No yang node found: %s", name); + clicon_err(OE_YANG, errno, "No yang node found: %s", name); goto done; } i++; @@ -627,13 +627,15 @@ text_create_modtree(char *api_path, return retval; } -/*! Check if child with fullmatch exists - * param[in] cvk vector of index keys +/*! Given a modification tree, check existing matching child in the base tree + * param[in] x0 Base tree node + * param[in] x1c Modification tree child + * param[in] yc Yang spec of tree child */ static cxobj * -find_match(cxobj *x0, - cxobj *x1c, - yang_stmt *yc) +match_base_child(cxobj *x0, + cxobj *x1c, + yang_stmt *yc) { cxobj *x0c = NULL; char *keyname; @@ -642,41 +644,54 @@ find_match(cxobj *x0, char *b0; char *b1; yang_stmt *ykey; - char *name; + char *cname; int ok; + char *x1bstr; /* body string */ - name = xml_name(x1c); - if (yc->ys_keyword != Y_LIST){ - x0c = xml_find(x0, name); - goto done; - } - if ((ykey = yang_find((yang_node*)yc, Y_KEY, NULL)) == NULL){ - clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", - __FUNCTION__, yc->ys_argument); - goto done; - } - /* The value is a list of keys: [ ]* */ - if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) - goto done; - x0c = NULL; - while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL) { - if (strcmp(xml_name(x0c), name)) - continue; - cvi = NULL; - ok = 0; - while ((cvi = cvec_each(cvk, cvi)) != NULL) { - keyname = cv_string_get(cvi); - ok = 1; /* if we come here */ - if ((b0 = xml_find_body(x0c, keyname)) == NULL) - break; /* error case */ - if ((b1 = xml_find_body(x1c, keyname)) == NULL) - break; /* error case */ - if (strcmp(b0, b1)) + cname = xml_name(x1c); + switch (yc->ys_keyword){ + case Y_LEAF_LIST: /* Match with name and value */ + x1bstr = xml_body(x1c); + x0c = NULL; + while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL) { + if (strcmp(cname, xml_name(x0c)) == 0 && + strcmp(xml_body(x0c), x1bstr)==0) break; - ok = 2; /* and reaches here for all keynames, x0c is found. */ } - if (ok == 2) - break; + break; + case Y_LIST: /* Match with key values */ + if ((ykey = yang_find((yang_node*)yc, Y_KEY, NULL)) == NULL){ + clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key", + __FUNCTION__, yc->ys_argument); + goto done; + } + /* The value is a list of keys: [ ]* */ + if ((cvk = yang_arg2cvec(ykey, " ")) == NULL) + goto done; + x0c = NULL; + while ((x0c = xml_child_each(x0, x0c, CX_ELMNT)) != NULL) { + if (strcmp(xml_name(x0c), cname)) + continue; + cvi = NULL; + ok = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + ok = 1; /* if we come here */ + if ((b0 = xml_find_body(x0c, keyname)) == NULL) + break; /* error case */ + if ((b1 = xml_find_body(x1c, keyname)) == NULL) + break; /* error case */ + if (strcmp(b0, b1)) + break; + ok = 2; /* and reaches here for all keynames, x0c is found. */ + } + if (ok == 2) + break; + } + break; + default: /* Just match with name */ + x0c = xml_find(x0, cname); + break; } done: if (cvk) @@ -745,14 +760,18 @@ text_modify(cxobj *x0, if (x0==NULL){ if ((x0 = xml_new_spec(name, x0p, y)) == NULL) goto done; - if ((x0b = xml_new("body", x0)) == NULL) - goto done; - xml_type_set(x0b, CX_BODY); + if (x1bstr){ /* empty type does not have body */ + if ((x0b = xml_new("body", x0)) == NULL) + goto done; + xml_type_set(x0b, CX_BODY); + } + } + if (x1bstr){ + if ((x0b = xml_body_get(x0)) == NULL) + goto done; + if (xml_value_set(x0b, x1bstr) < 0) + goto done; } - if ((x0b = xml_body_get(x0)) == NULL) - goto done; - if (xml_value_set(x0b, x1bstr) < 0) - goto done; break; default: break; @@ -785,16 +804,21 @@ text_modify(cxobj *x0, if ((x0 = xml_new_spec(name, x0p, y)) == NULL) goto done; } + /* Loop through children of the modification tree */ x1c = NULL; while ((x1c = xml_child_each(x1, x1c, CX_ELMNT)) != NULL) { cname = xml_name(x1c); - /* XXX This is more than find, if a list keys must match */ - + /* Get yang spec of the child */ if (y == NULL) yc = yang_find_topnode(yspec, cname); /* still NULL for config */ - else - yc = yang_find_syntax(y, cname); - x0c = find_match(x0, x1c, yc); + else{ + if ((yc = yang_find_syntax(y, cname)) == NULL){ + clicon_err(OE_YANG, errno, "No yang node found: %s", cname); + goto done; + } + } + /* See if there is a corresponding node in the base tree */ + x0c = yc?match_base_child(x0, x1c, yc):NULL; if (text_modify(x0c, x0, x1c, op, (yang_node*)yc, yspec) < 0) goto done; } @@ -890,7 +914,7 @@ text_put(xmldb_handle xh, /* here xt looks like: ... */ /* If xpath find first occurence or api-path (this is where we apply xml) */ if (api_path){ - if (text_create_modtree(api_path, xt, op, yspec, &xbase, &xbasep, &y) < 0) + if (text_apipath_modify(api_path, xt, op, yspec, &xbase, &xbasep, &y) < 0) goto done; } else{ diff --git a/test/test4.sh b/test/test4.sh index 38875a41..fb21a3bf 100755 --- a/test/test4.sh +++ b/test/test4.sh @@ -31,6 +31,9 @@ module ietf-ip{ type string; } } + leaf g { + type string; + } container h { leaf j { type string; diff --git a/test/test5.sh b/test/test5.sh index 812b2e3f..11c85521 100755 --- a/test/test5.sh +++ b/test/test5.sh @@ -30,6 +30,9 @@ module ietf-ip{ type string; } } + leaf g { + type string; + } container h { leaf j { type string; @@ -53,16 +56,42 @@ run(){ new "datastore $name init" expectfn "$datastore $conf init" "" - new "datastore $name put top" - echo "$datastore $conf put replace '/' $db" - expectfn "$datastore $conf put replace '/' \"$db\"" "" + new "datastore $name put all replace" + expectfn "$datastore $conf put replace / $db" "" + + new "datastore $name get" + expectfn "$datastore $conf get /" "^$db$" + + new "datastore $name put all remove" + expectfn "$datastore $conf put remove /" + + new "datastore $name get" + expectfn "$datastore $conf get /" "^$" + + new "datastore $name put all merge" + expectfn "$datastore $conf put merge / $db" "" + + new "datastore $name get" + expectfn "$datastore $conf get /" "^$db$" + + new "datastore $name put all delete" + expectfn "$datastore $conf put remove /" + + new "datastore $name get" + expectfn "$datastore $conf get /" "^$" + + new "datastore $name put all create" + expectfn "$datastore $conf put create / $db" "" + + new "datastore $name get" + expectfn "$datastore $conf get /" "^$db$" + + new "datastore $name put top create" + expectfn "$datastore $conf put create / " "" # error return - new "datastore $name get" - expectfn "$datastore $conf get /" $db - new "datastore $name put rm" - expectfn "$datastore $conf put remove /x/g" + new "datastore $name put top" expectfn "$datastore $conf put replace / $db" From 69ff0e38914f9c4e6ce6b80494b297e1792dac4b Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 1 May 2017 12:46:09 +0200 Subject: [PATCH 13/24] Refined netconf none semantics in tests and text datastore --- CHANGELOG.txt | 2 + README.md | 15 ++++- apps/backend/backend_client.c | 1 - apps/restconf/{README => README.md} | 4 +- apps/restconf/restconf_methods.c | 9 +-- datastore/keyvalue/clixon_keyvalue.c | 4 +- datastore/text/clixon_xmldb_text.c | 88 ++++++++++++++++++++++++---- example/routing.conf.local | 6 +- lib/clixon/clixon_xml.h | 2 + lib/clixon/clixon_xml_db.h | 2 +- lib/clixon/clixon_xml_map.h | 2 +- lib/src/clixon_xml.c | 9 +++ lib/src/clixon_xml_db.c | 7 ++- lib/src/clixon_xml_map.c | 23 +++++--- lib/src/clixon_xsl.c | 17 +++--- test/test1.sh | 2 + test/test2.sh | 48 ++++++++++----- test/test3.sh | 38 +++++++----- test/test5.sh | 59 ++++++++++++++----- 19 files changed, 247 insertions(+), 91 deletions(-) rename apps/restconf/{README => README.md} (92%) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 993727db..ce6aadd4 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -29,6 +29,8 @@ # # ***** END LICENSE BLOCK ***** +- Refined netconf "none" semantics in tests and text datastore + - Moved apps/dbctrl to datastore/ - Added connect/disconnect/getopt/setopt and handle to xmldb API diff --git a/README.md b/README.md index 28c148b6..47347b22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -CLIXON -====== +# CLIXON CLIXON is an automatic configuration manager where you from a YANG specification generate interactive CLI, NETCONF, RESTCONF and embedded @@ -12,6 +11,8 @@ applications such as CLICON/ROST does not run on CLIXON. Presentations and tutorial is found on the [CLICON project page](http://www.clicon.org) +## Installation + A typical installation is as follows: > configure # Configure clixon to platform @@ -22,17 +23,27 @@ A typical installation is as follows: One example applications is provided, the IETF IP YANG datamodel with generated CLI and configuration interface. It all origins from work at [KTH](http://www.csc.kth.se/~olofh/10G_OSR) +## Dependencies + [CLIgen](http://www.cligen.se) is required for building CLIXON. If you need to build and install CLIgen: git clone https://github.com/olofhagsand/cligen.git cd cligen; configure; make; make install +## Licenses + CLIXON is dual license. Either Apache License, Version 2.0 or GNU General Public License Version 2. You choose. See LICENSE.md for license, CHANGELOG for recent changes. +## Client code + +[CLI](apps/restconf). +[Restconf](apps/restconf). +[Netconf](apps/netconf). +[Netconf](apps/netconf). diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 591d0e33..f5e26f8d 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -307,7 +307,6 @@ from_client_edit_config(clicon_handle h, } } if ((xc = xpath_first(xn, "config")) != NULL){ - /* XXX see from_client_xmlput() */ if (xmldb_put(h, target, operation, api_path, xc) < 0){ cprintf(cbret, "" "operation-failed" diff --git a/apps/restconf/README b/apps/restconf/README.md similarity index 92% rename from apps/restconf/README rename to apps/restconf/README.md index 8d750288..d058225b 100644 --- a/apps/restconf/README +++ b/apps/restconf/README.md @@ -66,8 +66,8 @@ curl -sX POST -d '{"clicon":{"interfaces":{"interface":{"name":"eth1","type":"et 3. DEBUGGING ++++++++++++ -Start the restconf programs with debug flag: -sudo su -c "/www-data/clixon_restconf -D" -s /bin/sh www-data +Start the restconf fastcgi program with debug flag: +sudo su -c "/www-data/clixon_restconf -Df /usr/local/etc/routing.conf" -s /bin/sh www-data Look at syslog: tail -f /var/log/syslog | grep clixon_restconf diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index b96b72b6..3194c4d6 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -296,7 +296,7 @@ api_data_edit(clicon_handle h, goto done; } cprintf(cbx, ""); - clicon_debug(1, "%s cbx: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); + clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); if (clicon_rpc_edit_config(h, "candidate", operation, api_path, @@ -337,7 +337,8 @@ api_data_edit(clicon_handle h, If the data resource already exists, then the POST request MUST fail and a "409 Conflict" status-line MUST be returned. - * Netconf: (nc:operation="create") | invoke an RPC operation + * Netconf: (nc:operation="create") | invoke an RPC operation * @example + */ int api_data_post(clicon_handle h, @@ -359,8 +360,8 @@ api_data_post(clicon_handle h, * @param[in] pi Offset, where to start pcvec * @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] data Stream input data - * Example: - curl -X PUT -d {\"enabled\":\"false\"} http://127.0.0.1/restconf/data/interfaces/interface=eth1 + * @example + curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 * PUT: if the PUT request creates a new resource, diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 1b4d3dc8..46e9f0b0 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -652,7 +652,7 @@ kv_get(xmldb_handle xh, } /* Top is special case */ if (!xml_flag(xt, XML_FLAG_MARK)) - if (xml_tree_prune_unmarked(xt, NULL) < 0) + if (xml_tree_prune_flagged(xt, XML_FLAG_MARK, 1, NULL) < 0) goto done; if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0) goto done; @@ -1309,7 +1309,7 @@ kv_put(xmldb_handle xh, yang_spec *yspec; char *dbfilename = NULL; - if ((xml_child_nr(xt)==0 || xml_body(xt)!= NULL) && + if (xt && (xml_child_nr(xt)==0 || xml_body(xt)!= NULL) && api_path && strlen(api_path) && strcmp(api_path,"/")) return xmldb_put_xkey(kh, db, op, api_path, xml_body(xt)); if ((yspec = kh->kh_yangspec) == NULL){ diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 3710d96b..3de5bd2e 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -245,6 +245,32 @@ text_setopt(xmldb_handle xh, return retval; } +/*! Populate with spec + * @param[in] xt XML tree with some node marked + */ +int +xml_spec_populate(cxobj *x, + void *arg) +{ + int retval = -1; + yang_spec *yspec = (yang_spec*)arg; + char *name; + yang_stmt *y; /* yang node */ + cxobj *xp; /* xml parent */ + yang_stmt *yp; /* parent yang */ + + name = xml_name(x); + if ((xp = xml_parent(x)) != NULL && + (yp = xml_spec(xp)) != NULL) + y = yang_find_syntax((yang_node*)yp, xml_name(x)); + else + y = yang_find_topnode(yspec, name); /* still NULL for config */ + xml_spec_set(x, y); + retval = 0; + // done: + return retval; +} + /*! Get content of database using xpath. return a set of matching sub-trees * The function returns a minimal tree that includes all sub-trees that match * xpath. @@ -335,20 +361,20 @@ text_get(xmldb_handle xh, } /* Top is special case */ if (!xml_flag(xt, XML_FLAG_MARK)) - if (xml_tree_prune_unmarked(xt, NULL) < 0) + if (xml_tree_prune_flagged(xt, XML_FLAG_MARK, 1, NULL) < 0) goto done; if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0) goto done; + if (xml_apply(xt, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) + goto done; + /* XXX does not work for top-level */ + if (xml_apply(xt, CX_ELMNT, xml_order, NULL) < 0) + goto done; + if (xml_apply(xt, CX_ELMNT, xml_sanity, NULL) < 0) + goto done; - if (0){ /* No xml_spec(xt) */ - if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0) - goto done; - /* XXX does not work for top-level */ - if (xml_apply(xt, CX_ELMNT, xml_order, NULL) < 0) - goto done; - if (xml_apply(xt, CX_ELMNT, xml_sanity, NULL) < 0) - goto done; - } if (debug>1) clicon_xml2file(stderr, xt, 0, 1); if (xvec0 && xlen0){ @@ -755,11 +781,14 @@ text_modify(cxobj *x0, goto done; } /* Fall thru */ + case OP_NONE: /* XXX */ case OP_MERGE: case OP_REPLACE: if (x0==NULL){ if ((x0 = xml_new_spec(name, x0p, y)) == NULL) goto done; + if (op==OP_NONE) + xml_flag_set(x0, XML_FLAG_NONE); /* Mark for potential deletion */ if (x1bstr){ /* empty type does not have body */ if ((x0b = xml_new("body", x0)) == NULL) goto done; @@ -767,12 +796,24 @@ text_modify(cxobj *x0, } } if (x1bstr){ - if ((x0b = xml_body_get(x0)) == NULL) - goto done; + if ((x0b = xml_body_get(x0)) == NULL){ + if ((x0b = xml_new("body", x0)) == NULL) + goto done; + xml_type_set(x0b, CX_BODY); + } if (xml_value_set(x0b, x1bstr) < 0) goto done; } break; + case OP_DELETE: + if (x0==NULL){ + clicon_err(OE_XML, 0, "Object to delete does not exist"); + goto done; + } + case OP_REMOVE: + if (x0) + xml_purge(x0); + break; default: break; } /* switch op */ @@ -799,11 +840,15 @@ text_modify(cxobj *x0, xml_purge(x0); x0 = NULL; } + case OP_NONE: /* XXX */ case OP_MERGE: if (x0==NULL){ if ((x0 = xml_new_spec(name, x0p, y)) == NULL) goto done; + if (op==OP_NONE) + xml_flag_set(x0, XML_FLAG_NONE); /* Mark for potential deletion */ } + /* Loop through children of the modification tree */ x1c = NULL; while ((x1c = xml_child_each(x1, x1c, CX_ELMNT)) != NULL) { @@ -823,6 +868,15 @@ text_modify(cxobj *x0, goto done; } break; + case OP_DELETE: + if (x0==NULL){ + clicon_err(OE_XML, 0, "Object to delete does not exist"); + goto done; + } + case OP_REMOVE: + if (x0) + xml_purge(x0); + break; default: break; } /* CONTAINER switch op */ @@ -857,7 +911,7 @@ text_modify(cxobj *x0, * @endcode */ int -text_put(xmldb_handle xh, +text_put(xmldb_handle xh, char *db, enum operation_type op, char *api_path, @@ -877,10 +931,12 @@ text_put(xmldb_handle xh, cxobj *xnew = NULL; yang_node *y = NULL; +#if 0 /* Just ignore */ if ((op==OP_DELETE || op==OP_REMOVE) && xmod){ clicon_err(OE_XML, 0, "xml tree should be NULL for REMOVE/DELETE"); goto done; } +#endif if (text_db2file(th, db, &dbfile) < 0) goto done; if (dbfile==NULL){ @@ -942,6 +998,12 @@ text_put(xmldb_handle xh, else if (text_modify(xbase, xbasep, xmod, op, (yang_node*)y, yspec) < 0) goto done; + /* Remove NONE nodes if all subs recursively are also NONE */ + if (xml_tree_prune_flagged(xt, XML_FLAG_NONE, 0, NULL) <0) + goto done; + if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, + (void*)XML_FLAG_NONE) < 0) + goto done; // output: /* Print out top-level xml tree after modification to file */ if ((cb = cbuf_new()) == NULL){ diff --git a/example/routing.conf.local b/example/routing.conf.local index 8d705aba..698e56f4 100644 --- a/example/routing.conf.local +++ b/example/routing.conf.local @@ -26,4 +26,8 @@ CLICON_CLI_GENMODEL_TYPE VARS CLICON_CLIGEN_CALLBACK_SINGLE_ARG 0 # Enabled uses "startup" configuration on boot -CLICON_USE_STARTUP_CONFIG 0 \ No newline at end of file +CLICON_USE_STARTUP_CONFIG 0 + +# XMLDB datastore plugin filename (see datastore/ and clixon_xml_db.[ch]) +CLICON_XMLDB_PLUGIN /usr/local/lib/xmldb/text.so +#CLICON_XMLDB_PLUGIN /usr/local/lib/xmldb/keyvalue.so diff --git a/lib/clixon/clixon_xml.h b/lib/clixon/clixon_xml.h index 43ab57d9..cf40ff39 100644 --- a/lib/clixon/clixon_xml.h +++ b/lib/clixon/clixon_xml.h @@ -67,6 +67,7 @@ typedef int (xml_applyfn_t)(cxobj *yn, void *arg); #define XML_FLAG_ADD 0x02 /* Node is added (commits) or parent added rec*/ #define XML_FLAG_DEL 0x04 /* Node is deleted (commits) or parent deleted rec */ #define XML_FLAG_CHANGE 0x08 /* Node is changed (commits) or child changed rec */ +#define XML_FLAG_NONE 0x10 /* Node is added as NONE */ /* * Prototypes @@ -102,6 +103,7 @@ int xml_childvec_set(cxobj *x, int len); cxobj *xml_new(char *name, cxobj *xn_parent); cxobj *xml_new_spec(char *name, cxobj *xn_parent, void *spec); void *xml_spec(cxobj *x); +void *xml_spec_set(cxobj *x, void *spec); cxobj *xml_find(cxobj *xn_parent, char *name); int xml_addsub(cxobj *xp, cxobj *xc); diff --git a/lib/clixon/clixon_xml_db.h b/lib/clixon/clixon_xml_db.h index eced86bf..6b814f46 100644 --- a/lib/clixon/clixon_xml_db.h +++ b/lib/clixon/clixon_xml_db.h @@ -106,7 +106,7 @@ typedef int (xmldb_delete_t)(xmldb_handle xh, char *db); /* Type of xmldb init function */ typedef int (xmldb_init_t)(xmldb_handle xh, char *db); -/* grideye agent plugin init struct for the api */ +/* plugin init struct for the api */ struct xmldb_api{ int xa_version; int xa_magic; diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 1cabeff6..437ac46f 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -64,7 +64,7 @@ int xml_diff(yang_spec *yspec, cxobj *xt1, cxobj *xt2, int yang2xmlkeyfmt(yang_stmt *ys, int inclkey, char **xkfmt); int xmlkeyfmt2key(char *xkfmt, cvec *cvv, char **xk); int xmlkeyfmt2xpath(char *xkfmt, cvec *cvv, char **xk); -int xml_tree_prune_unmarked(cxobj *xt, int *upmark); +int xml_tree_prune_flagged(cxobj *xt, int flag, int test, int *upmark); int xml_default(cxobj *x, void *arg); int xml_order(cxobj *x, void *arg); int xml_sanity(cxobj *x, void *arg); diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index ce836af7..0b4d3835 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -521,12 +521,21 @@ xml_new_spec(char *name, return x; } + void * xml_spec(cxobj *x) { return x->x_spec; } +void * +xml_spec_set(cxobj *x, + void *spec) +{ + x->x_spec = spec; + return 0; +} + /*! Find an XML node matching name among a parent's children. * * Get first XML node directly under x_up in the xml hierarchy with diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index 0ebf7c8e..bbfc215f 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -63,6 +63,9 @@ #include "clixon_options.h" #include "clixon_xml_db.h" +/* Set to log get and put requests */ +#define DEBUG 0 + /*! Load an xmldb storage plugin according to filename * If init function fails (not found, wrong version, etc) print a log and dont * add it. @@ -350,7 +353,7 @@ xmldb_get(clicon_handle h, goto done; } retval = xa->xa_get_fn(xh, db, xpath, xtop, xvec, xlen); -#if 0 /* XXX DEBUG */ +#if DEBUG if (retval == 0) { cbuf *cb = cbuf_new(); clicon_xml2cbuf(cb, *xtop, 0, 0); @@ -407,7 +410,7 @@ xmldb_put(clicon_handle h, clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } -#if 0 /* XXX DEBUG */ +#if DEBUG { cbuf *cb = cbuf_new(); if (xt) diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index c89cc4c6..719eddf1 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1076,16 +1076,23 @@ xmlkeyfmt2xpath(char *xkfmt, return retval; } -/*! Prune everything that has not been marked +/*! Prune everything that does not pass test * @param[in] xt XML tree with some node marked + * @param[in] flag Which flag to test for + * @param[in] test 1: test that flag is set, 0: test that flag is not set * @param[out] upmark Set if a child (recursively) has marked set. - * The function removes all branches that does not contain a marked child - * XXX: maybe key leafs should not be purged if list is not purged? - * XXX: consider move to clicon_xml + * The function removes all branches that does not a child that pass the test + * Purge all nodes that dont have MARK flag set recursively. + * Save all nodes that is MARK:ed or have at least one (grand*)child that is MARKed + * @code + * xml_tree_prune_flagged(xt, XML_FLAG_MARK, 1, NULL); + * @endcode */ int -xml_tree_prune_unmarked(cxobj *xt, - int *upmark) +xml_tree_prune_flagged(cxobj *xt, + int flag, + int test, + int *upmark) { int retval = -1; int submark; @@ -1097,12 +1104,12 @@ xml_tree_prune_unmarked(cxobj *xt, x = NULL; xprev = x = NULL; while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { - if (xml_flag(x, XML_FLAG_MARK)){ + if (xml_flag(x, flag) == test?flag:0){ mark++; xprev = x; continue; /* mark and stop here */ } - if (xml_tree_prune_unmarked(x, &submark) < 0) + if (xml_tree_prune_flagged(x, flag, test, &submark) < 0) goto done; if (submark) mark++; diff --git a/lib/src/clixon_xsl.c b/lib/src/clixon_xsl.c index 441d15a4..b16da920 100644 --- a/lib/src/clixon_xsl.c +++ b/lib/src/clixon_xsl.c @@ -358,6 +358,13 @@ xpath_parse(char *xpath, else if (strncmp(s,"descendant-or-self::", strlen("descendant-or-self::"))==0){ xpath_element_new(A_DESCENDANT_OR_SELF, s+strlen("descendant-or-self::"), &xpnext); } +#if 1 + else if (strncmp(s,"..", strlen(".."))==0) /* abbreviatedstep */ + xpath_element_new(A_PARENT, s+strlen(".."), &xpnext); +#else + else if (strncmp(s,"..", strlen(s))==0) /* abbreviatedstep */ + xpath_element_new(A_PARENT, NULL, &xpnext); +#endif #if 1 /* Problems with .[userid=1321] */ else if (strncmp(s,".", strlen("."))==0) xpath_element_new(A_SELF, s+strlen("."), &xpnext); @@ -368,13 +375,7 @@ xpath_parse(char *xpath, else if (strncmp(s,"self::", strlen("self::"))==0) xpath_element_new(A_SELF, s+strlen("self::"), &xpnext); -#if 1 - else if (strncmp(s,"..", strlen(".."))==0) /* abbreviatedstep */ - xpath_element_new(A_PARENT, s+strlen(".."), &xpnext); -#else - else if (strncmp(s,"..", strlen(s))==0) /* abbreviatedstep */ - xpath_element_new(A_PARENT, NULL, &xpnext); -#endif + else if (strncmp(s,"parent::", strlen("parent::"))==0) xpath_element_new(A_PARENT, s+strlen("parent::"), &xpnext); else if (strncmp(s,"ancestor::", strlen("ancestor::"))==0) @@ -1076,7 +1077,7 @@ int main(int argc, char **argv) { int i; - cxobj **xv; + cxobj **xv cxobj *x; cxobj *xn; size_t xlen = 0; diff --git a/test/test1.sh b/test/test1.sh index 24d30a9f..b75e8434 100755 --- a/test/test1.sh +++ b/test/test1.sh @@ -26,6 +26,8 @@ sudo clixon_backend -If $clixon_cf if [ $? -ne 0 ]; then err fi +new "cli tests" + new "cli configure top" expectfn "$clixon_cli -1f $clixon_cf set interfaces" "" diff --git a/test/test2.sh b/test/test2.sh index 0267e87c..e6066427 100755 --- a/test/test2.sh +++ b/test/test2.sh @@ -20,14 +20,44 @@ sudo clixon_backend -If $clixon_cf if [ $? -ne 0 ]; then err fi + +new "netconf tests" + new "netconf get empty config" -expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^]]>]]>$" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' '^]]>]]>$' + +new "Add subtree eth0 using none which should not change anything" +expecteof "$clixon_netconf -qf $clixon_cf" "noneeth0]]>]]>" "^]]>]]>$" + +new "Check nothing added" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' '^]]>]]>$' + +new "Add subtree eth0 using none and create which should add eth0" +expecteof "$clixon_netconf -qf $clixon_cf" 'eth0ethnone ]]>]]>' "^]]>]]>$" + +new "Check eth0 added using xpath" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' "^eth0ethtrue]]>]]>$" + +new "Re-create same eth0 which should generate error" +expecteof "$clixon_netconf -qf $clixon_cf" 'eth0ethnone ]]>]]>' "^" + +new "Delete eth0 using none config" +expecteof "$clixon_netconf -qf $clixon_cf" 'eth0ethnone ]]>]]>' "^]]>]]>$" + +new "Check deleted eth0" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' '^]]>]]>$' + +new "Re-Delete eth0 using none should generate error" +expecteof "$clixon_netconf -qf $clixon_cf" 'eth0ethnone ]]>]]>' "^" new "netconf edit config" expecteof "$clixon_netconf -qf $clixon_cf" "eth0eth1true
9.2.3.424
]]>]]>" "^]]>]]>$" new "netconf get config xpath" -expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^true]]>]]>$" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' "^true]]>]]>$" + +new "netconf get config xpath parent" +expecteof "$clixon_netconf -qf $clixon_cf" ']]>]]>' "^eth0trueeth1truetruefalse
9.2.3.424
]]>]]>$" new "netconf validate missing type" expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^" @@ -48,23 +78,11 @@ new "netconf commit" expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^]]>]]>$" new "netconf edit config replace" -expecteof "$clixon_netconf -qf $clixon_cf" "eth2ethmerge ]]>]]>" "^]]>]]>$" +expecteof "$clixon_netconf -qf $clixon_cf" "eth2ethmerge]]>]]>" "^]]>]]>$" new "netconf get replaced config" expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^eth1ethtrueeth2ethtrue]]>]]>$" -new "netconf edit config create" -expecteof "$clixon_netconf -qf $clixon_cf" 'eth3ethnone ]]>]]>' "^]]>]]>$" - -new "netconf edit config create 2nd" -expecteof "$clixon_netconf -qf $clixon_cf" 'eth3ethmerge ]]>]]>' "^" - -new "netconf edit config delete" -expecteof "$clixon_netconf -qf $clixon_cf" 'eth3ethnone ]]>]]>' "^]]>]]>$" - -new "netconf get delete config" -expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^eth1ethtrueeth2ethtrue]]>]]>$" - new "netconf discard-changes" expecteof "$clixon_netconf -qf $clixon_cf" "]]>]]>" "^]]>]]>$" diff --git a/test/test3.sh b/test/test3.sh index 0c04bdc6..ed1335e7 100755 --- a/test/test3.sh +++ b/test/test3.sh @@ -24,32 +24,40 @@ sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c sleep 1 +new "restconf tests" + new "restconf options" expectfn "curl -i -s -X OPTIONS http://localhost/restconf/data" "Allow: OPTIONS,HEAD,GET,POST,PUT,DELETE" -new "restconf get empty config" -expectfn "curl -sG http://localhost/restconf/data" "^null $" - -new "restconf put config" -expectfn 'curl -sX POST -d {"interfaces":{"interface":[{"name":"eth1","type":"eth","enabled":"true"},{"name":"eth0","type":"eth","enabled":"true"}]}} http://localhost/restconf/data' "" - -new "restconf get config" -expectfn "curl -sG http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth1","type": "eth","enabled": "true"},{ "name": "eth0","type": "eth","enabled": "true"}\]}} -$' - new "restconf head" expectfn "curl -s -I http://localhost/restconf/data" "Content-Type: application/yang.data\+json" -new "restconf POST config" -expectfn 'curl -sX POST -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth4' "" +new "restconf get empty config" +expectfn "curl -sG http://localhost/restconf/data" "^null $" -new "restconf DELETE config" +# +new "Add subtree eth0,eth1 using POST" +expectfn 'curl -sX POST -d {"interfaces":{"interface":[{"name":"eth0","type":"eth","enabled":"true"},{"name":"eth1","type":"eth","enabled":"true"}]}} http://localhost/restconf/data' "" + +new "Check eth0 added" +expectfn "curl -sG http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth0","type": "eth","enabled": "true"},{ "name": "eth1","type": "eth","enabled": "true"}\]}} +$' + +new "Re-post eth0 which should generate error" +expectfn 'curl -sX POST -d {"interfaces":{"interface":{"name":"eth0","type":"eth","enabled":"true"}}} http://localhost/restconf/data' "Not Found" + +new "delete eth0" expectfn 'curl -sX DELETE http://localhost/restconf/data/interfaces/interface=eth0' "" -new "restconf get config" -expectfn "curl -sG http://localhost/restconf/data" '{"interfaces": {"interface": \[{"name": "eth1","type": "eth","enabled": "true"},{ "name": "eth4","type": "eth","enabled": "true"}\]}} +new "Check deleted eth0" +expectfn 'curl -sG http://localhost/restconf/data' '{"interfaces": {"interface": {"name": "eth1","type": "eth","enabled": "true"}}} $' +new "Re-Delete eth0 using none should generate error" +expectfn 'curl -sX DELETE http://localhost/restconf/data/interfaces/interface=eth0' "Not Found" + +return + new "restconf PATCH config" expectfn 'curl -sX PATCH -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth4' "" diff --git a/test/test5.sh b/test/test5.sh index 11c85521..657ddc50 100755 --- a/test/test5.sh +++ b/test/test5.sh @@ -47,15 +47,18 @@ db='12first-entry13se run(){ name=$1 dir=/tmp/$name + if [ ! -d $dir ]; then mkdir $dir fi rm -rf $dir/* + conf="-d candidate -b $dir -p ../datastore/$name/$name.so -y /tmp -m ietf-ip" - echo "conf:$conf" +# echo "conf:$conf" new "datastore $name init" expectfn "$datastore $conf init" "" + # Whole tree operations new "datastore $name put all replace" expectfn "$datastore $conf put replace / $db" "" @@ -89,31 +92,55 @@ run(){ new "datastore $name put top create" expectfn "$datastore $conf put create / " "" # error -return + # Single key operations + # leaf + new "datastore $name put all delete" + expectfn "$datastore $conf delete" "" + new "datastore $name init" + expectfn "$datastore $conf init" "" + new "datastore $name create leaf" + expectfn "$datastore $conf put create /x/y=1,3/c newentry" - new "datastore $name put top" - expectfn "$datastore $conf put replace / $db" + new "datastore $name create leaf" + expectfn "$datastore $conf put create /x/y=1,3/c newentry" - new "datastore $name put del" + new "datastore $name delete leaf" + expectfn "$datastore $conf put delete /x/y=1,3" + + new "datastore $name replace leaf" + expectfn "$datastore $conf put create /x/y=1,3/c newentry" + + new "datastore $name remove leaf" + expectfn "$datastore $conf put remove /x/g" + + new "datastore $name remove leaf" + expectfn "$datastore $conf put remove /x/y=1,3/c" + + new "datastore $name delete leaf" expectfn "$datastore $conf put delete /x/g" -return - new "datastore $name get empty" - expectfn "$datastore $conf get /" "^$" + new "datastore $name merge leaf" + expectfn "$datastore $conf put merge /x/g nalle" - new "datastore $name put top" - expectfn "$datastore $conf put replace / foobarfie" "" + new "datastore $name replace leaf" + expectfn "$datastore $conf put replace /x/g nalle" - new "datastore $name get config" - expectfn "$datastore $conf get /" "^foobarfie$" + new "datastore $name merge leaf" + expectfn "$datastore $conf put merge /x/y=1,3/c newentry" - new "datastore $name put delete" - expectfn "$datastore $conf put delete / " "" + new "datastore $name replace leaf" + expectfn "$datastore $conf put replace /x/y=1,3/c newentry" - new "datastore $name get deleted" - expectfn "$datastore $conf get /" "^$" + new "datastore $name create leaf" + expectfn "$datastore $conf put create /x/h aaa" + + new "datastore $name create leaf" + expectfn "$datastore $conf put create /x/y=1,3/c newentry" + +#leaf-list + rm -rf $dir } From a285b142221a669afaa1020bd16c1940e20acf9b Mon Sep 17 00:00:00 2001 From: Olof Hagsand Date: Mon, 1 May 2017 16:01:14 +0200 Subject: [PATCH 14/24] delq --- apps/cli/cli_plugin.c | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/cli/cli_plugin.c b/apps/cli/cli_plugin.c index 77252d83..dcdb1730 100644 --- a/apps/cli/cli_plugin.c +++ b/apps/cli/cli_plugin.c @@ -166,11 +166,12 @@ syntax_append(clicon_handle h, * Unload all plugins in a group */ static int -syntax_unload(clicon_handle h) +cli_syntax_unload(clicon_handle h) { - struct cli_plugin *p; - cli_syntax_t *stx = cli_syntax(h); - + cli_syntax_t *stx = cli_syntax(h); + struct cli_plugin *p; + cli_syntaxmode_t *m; + if (stx == NULL) return 0; @@ -178,15 +179,16 @@ syntax_unload(clicon_handle h) p = stx->stx_plugins; plugin_unload(h, p->cp_handle); clicon_debug(1, "DEBUG: Plugin '%s' unloaded.", p->cp_name); - DELQ(stx->stx_plugins, stx->stx_plugins, struct cli_plugin *); - if (stx->stx_plugins) - free(stx->stx_plugins); + DELQ(p, stx->stx_plugins, struct cli_plugin *); + if (p) + free(p); stx->stx_nplugins--; } while (stx->stx_nmodes > 0) { - DELQ(stx->stx_modes, stx->stx_modes, cli_syntaxmode_t *); - if (stx->stx_modes) - free(stx->stx_modes); + m = stx->stx_modes; + DELQ(m, stx->stx_modes, cli_syntaxmode_t *); + if (m) + free(m); stx->stx_nmodes--; } return 0; @@ -509,7 +511,7 @@ cli_syntax_load (clicon_handle h) quit: if (retval != 0) { - syntax_unload(h); + cli_syntax_unload(h); cli_syntax_set(h, NULL); } if (dp) @@ -547,7 +549,7 @@ cli_plugin_start(clicon_handle h, int argc, char **argv) int cli_plugin_finish(clicon_handle h) { - syntax_unload(h); + cli_syntax_unload(h); cli_syntax_set(h, NULL); return 0; } From a2c2375b387527c159b3b2fb29e8caa1198f3cc5 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 1 May 2017 16:50:22 +0200 Subject: [PATCH 15/24] docs --- CHANGELOG.txt | 2 ++ README.md | 31 ++++++++++++++++--------------- apps/netconf/README.md | 17 +++++++++++++++++ apps/restconf/README.md | 40 +++++++++++++++++++++++++--------------- clixon.conf.cpp.cpp | 2 +- datastore/README.md | 16 ++++++++++++++++ 6 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 apps/netconf/README.md create mode 100644 datastore/README.md diff --git a/CHANGELOG.txt b/CHANGELOG.txt index ce6aadd4..84df96ff 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -29,6 +29,8 @@ # # ***** END LICENSE BLOCK ***** +- Datastore text module is now the default. + - Refined netconf "none" semantics in tests and text datastore - Moved apps/dbctrl to datastore/ diff --git a/README.md b/README.md index 47347b22..4b5a5845 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,10 @@ -# CLIXON +# Clixon -CLIXON is an automatic configuration manager where you from a YANG +Clixon is an automatic configuration manager where you from a YANG specification generate interactive CLI, NETCONF, RESTCONF and embedded databases with transaction support. -CLIXON is a fork of CLICON where legacy key specification has been -replaced completely by YANG. This means that legacy CLICON -applications such as CLICON/ROST does not run on CLIXON. - -Presentations and tutorial is found on the [CLICON project -page](http://www.clicon.org) +Presentations and tutorial is found on the [CLICON project page](http://www.clicon.org) ## Installation @@ -20,8 +15,13 @@ A typical installation is as follows: > sudo make install # Install libs, binaries, and config-files > sudo make install-include # Install include files (for compiling) -One example applications is provided, the IETF IP YANG datamodel with generated CLI and configuration interface. It all origins from work at -[KTH](http://www.csc.kth.se/~olofh/10G_OSR) +One example applications is provided, a IETF IP YANG datamodel with generated CLI and configuration interface. + +## More info + +- [Datastore](datastore). +- [Restconf](apps/restconf). +- [Netconf](apps/netconf). ## Dependencies @@ -38,12 +38,13 @@ General Public License Version 2. You choose. See LICENSE.md for license, CHANGELOG for recent changes. -## Client code +## Related -[CLI](apps/restconf). -[Restconf](apps/restconf). -[Netconf](apps/netconf). -[Netconf](apps/netconf). +CLIXON is a fork of CLICON where legacy key specification has been +replaced completely by YANG. This means that legacy CLICON +applications such as CLICON/ROST does not run on CLIXON. + +Clixon origins from work at [KTH](http://www.csc.kth.se/~olofh/10G_OSR) diff --git a/apps/netconf/README.md b/apps/netconf/README.md new file mode 100644 index 00000000..3878aa56 --- /dev/null +++ b/apps/netconf/README.md @@ -0,0 +1,17 @@ +# Clixon Netconf + +Clixon netconf implements the following standards: +- RFC 4741 (NETCONF Configuration Protocol), +- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) and +- RFC 5277 (NETCONF Event Notifications). + +It needs to be updated to RFC6241 and RFC 6242. It also does not implement the following features: + +- :url capability +- copy-config source config +- edit-config testopts +- edit-config erropts +- edit-config config-text + + + diff --git a/apps/restconf/README.md b/apps/restconf/README.md index d058225b..36916db1 100644 --- a/apps/restconf/README.md +++ b/apps/restconf/README.md @@ -1,13 +1,12 @@ -Clixon Restconf -=============== +# Clixon Restconf Contents: 1. Features 2. Installation using NGINX 3. Debugging -1. FEATURES -+++++++++++ +## 1. FEATURES + Clixon restconf is a daemon based on FASTCGI. Instructions are available to run with NGINX. The implementatation supports plain OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE. @@ -18,10 +17,10 @@ including: - notifications (sec 6) - only rudimentary error reporting exists (sec 7) -2. INSTALLATION using NGINX -+++++++++++++++++++++++++++ +## 2. INSTALLATION using NGINX -# Define nginx config file/etc/nginx/sites-available/default +Define nginx config file/etc/nginx/sites-available/default +``` server { ... location /restconf { @@ -30,13 +29,19 @@ server { include fastcgi_params; } } -# Start nginx daemon +``` +Start nginx daemon +``` sudo /etc/init.d nginx start +``` -# Start clixon restconf daemon +Start clixon restconf daemon +``` olof@vandal> sudo su -c "/www-data/clixon_restconf -f /usr/local/etc/routing.conf " -s /bin/sh www-data +``` -# Make restconf calls with curl +Make restconf calls with curl +``` olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces [ { @@ -62,16 +67,21 @@ olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces/interface/name=et ] curl -sX POST -d '{"clicon":{"interfaces":{"interface":{"name":"eth1","type":"eth","enabled":"true"}}}}' http://localhost/restconf/data +``` +## DEBUGGING -3. DEBUGGING -++++++++++++ Start the restconf fastcgi program with debug flag: -sudo su -c "/www-data/clixon_restconf -Df /usr/local/etc/routing.conf" -s /bin/sh www-data - +``` +sudo su -c "/www-data/clixon_restconf -Df /usr/local/etc/routing.conf" -s /bin/sh www- +data +``` Look at syslog: +``` tail -f /var/log/syslog | grep clixon_restconf +``` Send command: +``` curl -G http://127.0.0.1/restconf/data/* - +``` diff --git a/clixon.conf.cpp.cpp b/clixon.conf.cpp.cpp index e6481452..1a6f441d 100644 --- a/clixon.conf.cpp.cpp +++ b/clixon.conf.cpp.cpp @@ -118,7 +118,7 @@ CLICON_BACKEND_PIDFILE localstatedir/APPNAME/APPNAME.pidfile CLICON_XMLDB_DIR localstatedir/APPNAME # XMLDB datastore plugin filename (see datastore/ and clixon_xml_db.[ch]) -CLICON_XMLDB_PLUGIN libdir/xmldb/keyvalue.so +CLICON_XMLDB_PLUGIN libdir/xmldb/text.so # Dont include keys in cvec in cli vars callbacks, ie a & k in 'a k ' ignored # CLICON_CLI_VARONLY 1 diff --git a/datastore/README.md b/datastore/README.md new file mode 100644 index 00000000..144446c9 --- /dev/null +++ b/datastore/README.md @@ -0,0 +1,16 @@ +# Clixon datastore + +The Clixon datastore is a stand-alone XML based datastore used by +Clixon. The idea is to be able to use different datastores. There is +currently a Key-value plugin based on qdbm and a plain text-file +datastore. + +The datastore is primarily designed to be used by Clixon but can be used +separately. See datastore_client.c for an example of how to use a +datastore plugin for other applications + +Can we equate a file that does not exist with an empty file? +Or is empty file same as ? +Three states: +NULL <---> "" <---> "" +which are valid? From 4bcb966dac40f4d567b5ad861f66199aa164e5be Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 2 May 2017 22:29:56 +0200 Subject: [PATCH 16/24] xmldb_init -> xmldb_create --- README.md | 236 +++++++++++++++++++++++++++++++--- apps/backend/backend_client.c | 2 +- apps/backend/backend_main.c | 8 +- datastore/README.md | 61 ++++++++- datastore/datastore_client.c | 2 +- lib/clixon/clixon_xml_db.h | 6 +- lib/src/clixon_xml_db.c | 6 +- 7 files changed, 288 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4b5a5845..48780720 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,44 @@ databases with transaction support. Presentations and tutorial is found on the [CLICON project page](http://www.clicon.org) -## Installation +## 1. Installation A typical installation is as follows: - - > configure # Configure clixon to platform - > make # Compile - > sudo make install # Install libs, binaries, and config-files - > sudo make install-include # Install include files (for compiling) - +``` + configure # Configure clixon to platform + make # Compile + sudo make install # Install libs, binaries, and config-files + sudo make install-include # Install include files (for compiling) +``` One example applications is provided, a IETF IP YANG datamodel with generated CLI and configuration interface. -## More info +## 2. Documentation -- [Datastore](datastore). -- [Restconf](apps/restconf). -- [Netconf](apps/netconf). +- [Frequently asked questions](http://www.clicon.org/FAQ.html) +- [Reference manual(http://www.clicon.org/doxygen/index.html) (may not be 100%% synched) -## Dependencies +## 3. Dependencies -[CLIgen](http://www.cligen.se) is required for building CLIXON. If you need +Clixon is dependend on the following packages +- [CLIgen](http://www.cligen.se) is required for building CLIXON. If you need to build and install CLIgen: - +``` git clone https://github.com/olofhagsand/cligen.git cd cligen; configure; make; make install +``` +- Yacc/bison +- Lex/Flex +- Fcgi (if restconf is enabled) +- Qdbm key-value store (if keyvalue datastore is enabled) -## Licenses +## 4. Licenses CLIXON is dual license. Either Apache License, Version 2.0 or GNU General Public License Version 2. You choose. See LICENSE.md for license, CHANGELOG for recent changes. -## Related +## 5. History CLIXON is a fork of CLICON where legacy key specification has been replaced completely by YANG. This means that legacy CLICON @@ -46,5 +51,204 @@ applications such as CLICON/ROST does not run on CLIXON. Clixon origins from work at [KTH](http://www.csc.kth.se/~olofh/10G_OSR) +## 6. Clixon Datastore +The Clixon datastore is a stand-alone XML based datastore used by +Clixon. The idea is to be able to use different datastores. There is +currently a key-value plugin based on qdbm and a plain text-file +datastore. +The datastore is primarily designed to be used by Clixon but can be used +separately. + +A datastore is a dynamic plugin that is loaded at runtime with a +well-defined API. This means it is possible to create your own +datastore and plug it in a Clixon backend at runtime. + +### The functional API +``` +int xmldb_plugin_load(clicon_handle h, char *filename); +int xmldb_plugin_unload(clicon_handle h); +int xmldb_connect(clicon_handle h); +int xmldb_disconnect(clicon_handle h); +int xmldb_getopt(clicon_handle h, char *optname, void **value); +int xmldb_setopt(clicon_handle h, char *optname, void *value); +int xmldb_get(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen); +int xmldb_put(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt); +int xmldb_copy(clicon_handle h, char *from, char *to); +int xmldb_lock(clicon_handle h, char *db, int pid); +int xmldb_unlock(clicon_handle h, char *db); +int xmldb_unlock_all(clicon_handle h, int pid); +int xmldb_islocked(clicon_handle h, char *db); +int xmldb_exists(clicon_handle h, char *db); +int xmldb_delete(clicon_handle h, char *db); +int xmldb_create(clicon_handle h, char *db); +``` + +### Using the API + +To use the API, a client needs the following: +- A clicon handle. +- A datastore plugin, such as a text.so or keyvalue.so. These are normally built and installed at Clixon make. +- A directory where to store databases +- A yang specification. This needs to be parsed using the Clixon yang_parse() method. + +A client calling the API needs to (1)load a plugin and (2)connect to a +datastore. You can connect to several datastores, even concurrently, +but in practice in Clixon, you connect to a single store. + +After connecting to a datastore, you can create and modify databases +within the datastore, and set and get options of the datastore itself. + +When done, you disconnect from the datastore and unload the plugin. + +Within a datastore, the following four databases may exist: +- running +- candidate +- startup +- tmp + +Initially, a database does not exist but is created by +xmldb_create(). It is deleted by xmldb_delete(). You may check for +existence with xmldb_exists(). You need to create a database before +you can perform any data access on it. + +You may lock a database for exclusive modification according to +Netconf semantics. You may also unlock a single dabase, unlock all frm +a specific session. + +You can read a database with xmldb_get() and modify a database with +xmldb_put(), and xmldb_copy(). + +A typical datastore session can be as follows, see the source code of +datastore_client.c for a more elaborate example. + +``` + h = clicon_handle_init(); + xmldb_plugin_load(h, plugin); + xmldb_connect(h); + xmldb_setopt(h, "dbdir", dbdir); + xmldb_setopt(h, "yangspec", yspec); + /* From here databases in the datastore may be accessed */ + xmldb_create(h, "candidate"); + xmldb_copy(h, "running", "candidate"); + xmldb_lock(h, "candidate", 7878); + xmldb_put(h, "candidate", OP_CREATE, "/interfaces/interface=eth0", xml); + xmldb_unlock(h, "candidate"); + xmldb_get(h, "candidate", "/", &xml, &xvec, &xlen); + xmldb_disconnect(h) + xmdlb_plugin_unload(h); +``` + +## 7. YANG + +Clixon implements YANG RFC 6020. Clixon generates an interactive CLI +for YANG specifications. It also provides Restconf and Netconf clients. + +Clixon YANG currently does not provide the following support: +- type object-references +- if-feature +- unique +- rpc + +## 8. Netconf + +Clixon Netconf implements the following NETCONF standards: +- RFC 4741 (NETCONF Configuration Protocol) +- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) +- RFC 5277 (NETCONF Event Notifications) + +It needs to be updated to RFC6241 and RFC 6242. + +Clixon NETCONF currently does not support the following Netconf features: + +- :url capability +- copy-config source config +- edit-config testopts +- edit-config erropts +- edit-config config-text + +## 9. Restconf + +### Features + +Clixon restconf is a daemon based on FASTCGI. Instructions are available to +run with NGINX. +The implementatation supports plain OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE. +and is based on draft-ietf-netconf-restconf-13. +There is currently (2017) a RFC 8040, many of those features are _not_ implemented, +including: +- query parameters (section 4.9) +- notifications (sec 6) +- only rudimentary error reporting exists (sec 7) + +### Installation using Nginx + +Define nginx config file/etc/nginx/sites-available/default +``` +server { + ... + location /restconf { + root /usr/share/nginx/html/restconf; + fastcgi_pass unix:/www-data/fastcgi_restconf.sock; + include fastcgi_params; + } +} +``` +Start nginx daemon +``` +sudo /etc/init.d nginx start +``` + +Start clixon restconf daemon +``` +olof@vandal> sudo su -c "/www-data/clixon_restconf -f /usr/local/etc/routing.conf " -s /bin/sh www-data +``` + +Make restconf calls with curl +``` +olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces +[ + { + "interfaces": { + "interface":[ + { + "name": "eth0", + "type": "eth", + "enabled": "true", + "name": "eth9", + "type": "eth", + "enabled": "true" + } + ] + } + } +] +olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces/interface/name=eth9/type +[ + { + "type": "eth" + } +] + +curl -sX POST -d '{"clicon":{"interfaces":{"interface":{"name":"eth1","type":"eth","enabled":"true"}}}}' http://localhost/restconf/data +``` + +### Debugging + +Start the restconf fastcgi program with debug flag: +``` +sudo su -c "/www-data/clixon_restconf -Df /usr/local/etc/routing.conf" -s /bin/sh www- +data +``` +Look at syslog: +``` +tail -f /var/log/syslog | grep clixon_restconf +``` + +Send command: +``` +curl -G http://127.0.0.1/restconf/data/* +``` diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index f5e26f8d..924b444c 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -632,7 +632,7 @@ from_client_delete_config(clicon_handle h, "", clicon_err_reason); goto ok; } - if (xmldb_init(h, target) < 0){ + if (xmldb_create(h, target) < 0){ cprintf(cbret, "" "operation-failed" "protocol" diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index d3973ac6..4600eedd 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -159,7 +159,7 @@ db_reset(clicon_handle h, { if (xmldb_delete(h, db) != 0 && errno != ENOENT) return -1; - if (xmldb_init(h, db) < 0) + if (xmldb_create(h, db) < 0) return -1; return 0; } @@ -181,7 +181,7 @@ rundb_main(clicon_handle h, cxobj *xt = NULL; cxobj *xn; - if (xmldb_init(h, "tmp") < 0) + if (xmldb_create(h, "tmp") < 0) goto done; if (xmldb_copy(h, "running", "tmp") < 0){ clicon_err(OE_UNIX, errno, "file copy"); @@ -538,7 +538,7 @@ main(int argc, char **argv) else if (db_reset(h, "running") < 0) goto done; - if (xmldb_init(h, "candidate") < 0) + if (xmldb_create(h, "candidate") < 0) goto done; if (xmldb_copy(h, "running", "candidate") < 0) goto done; @@ -562,7 +562,7 @@ main(int argc, char **argv) } /* If candidate does not exist, create it from running */ if (xmldb_exists(h, "candidate") != 1){ - if (xmldb_init(h, "candidate") < 0) + if (xmldb_create(h, "candidate") < 0) goto done; if (xmldb_copy(h, "running", "candidate") < 0) goto done; diff --git a/datastore/README.md b/datastore/README.md index 144446c9..f9baa58b 100644 --- a/datastore/README.md +++ b/datastore/README.md @@ -9,8 +9,59 @@ The datastore is primarily designed to be used by Clixon but can be used separately. See datastore_client.c for an example of how to use a datastore plugin for other applications -Can we equate a file that does not exist with an empty file? -Or is empty file same as ? -Three states: -NULL <---> "" <---> "" -which are valid? +## The functional API +``` +int xmldb_plugin_load(clicon_handle h, char *filename); +int xmldb_plugin_unload(clicon_handle h); +int xmldb_connect(clicon_handle h); +int xmldb_disconnect(clicon_handle h); +int xmldb_getopt(clicon_handle h, char *optname, void **value); +int xmldb_setopt(clicon_handle h, char *optname, void *value); +int xmldb_get(clicon_handle h, char *db, char *xpath, + cxobj **xtop, cxobj ***xvec, size_t *xlen); +int xmldb_put(clicon_handle h, char *db, enum operation_type op, + char *api_path, cxobj *xt); +int xmldb_copy(clicon_handle h, char *from, char *to); +int xmldb_lock(clicon_handle h, char *db, int pid); +int xmldb_unlock(clicon_handle h, char *db); +int xmldb_unlock_all(clicon_handle h, int pid); +int xmldb_islocked(clicon_handle h, char *db); +int xmldb_exists(clicon_handle h, char *db); +int xmldb_delete(clicon_handle h, char *db); +int xmldb_init(clicon_handle h, char *db); +``` + +## Using the API + +This section described how the API is used. Please refer to datastore/datastore_client.c for an example of an actual implementation. + +### Prerequisities +To use the API, a client needs the following: +- A clicon handle. +- A datastore plugin, such as a text.so or keyvalue.so. These are normally built and installed at Clixon make. +- A directory where to store the datastores +- A yang specification. This needs to be parsed using the Clixon yang_parse() method. + +### Dynamics + +A client calling the API needs to load a plugin and connect to a +datastore. In principle, you cannot connect to several datastores, +even concurrently, but in practice in Clixon, you connect to a single +store. + +Within a datastore, there may be + +``` + h = clicon_handle_init(); + xmldb_plugin_load(h, plugin); +``` +The plugin is the complete path-name of a file. + +Thereafter, a connection is made to a specific plugin, such as a text plugin. It is possible to connect to several datastore at once, although this is not supported by CLixon: +``` +xmldb_connect(h); +xmldb_setopt(h, "dbdir", dbdir); +xmldb_setopt(h, "yangspec", yspec); +``` + + diff --git a/datastore/datastore_client.c b/datastore/datastore_client.c index 01eb48b1..d9ceaf61 100644 --- a/datastore/datastore_client.c +++ b/datastore/datastore_client.c @@ -286,7 +286,7 @@ main(int argc, char **argv) else if (strcmp(cmd, "init")==0){ if (argc != 1) usage(argv0); - if (xmldb_init(h, db) < 0) + if (xmldb_create(h, db) < 0) goto done; } else{ diff --git a/lib/clixon/clixon_xml_db.h b/lib/clixon/clixon_xml_db.h index 6b814f46..87f65411 100644 --- a/lib/clixon/clixon_xml_db.h +++ b/lib/clixon/clixon_xml_db.h @@ -104,7 +104,7 @@ typedef int (xmldb_exists_t)(xmldb_handle xh, char *db); typedef int (xmldb_delete_t)(xmldb_handle xh, char *db); /* Type of xmldb init function */ -typedef int (xmldb_init_t)(xmldb_handle xh, char *db); +typedef int (xmldb_create_t)(xmldb_handle xh, char *db); /* plugin init struct for the api */ struct xmldb_api{ @@ -125,7 +125,7 @@ struct xmldb_api{ xmldb_islocked_t *xa_islocked_fn; xmldb_exists_t *xa_exists_fn; xmldb_delete_t *xa_delete_fn; - xmldb_init_t *xa_init_fn; + xmldb_create_t *xa_create_fn; }; /* @@ -150,6 +150,6 @@ int xmldb_unlock_all(clicon_handle h, int pid); int xmldb_islocked(clicon_handle h, char *db); int xmldb_exists(clicon_handle h, char *db); int xmldb_delete(clicon_handle h, char *db); -int xmldb_init(clicon_handle h, char *db); +int xmldb_create(clicon_handle h, char *db); #endif /* _CLIXON_XML_DB_H */ diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index bbfc215f..edddd975 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -652,15 +652,15 @@ xmldb_delete(clicon_handle h, return retval; } -/*! Initialize database. Open database for writing. +/*! Create a database. Open database for writing. * @param[in] h Clicon handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -xmldb_init(clicon_handle h, - char *db) +xmldb_create(clicon_handle h, + char *db) { int retval = -1; xmldb_handle xh; From 7bda16eed52dc39470d41ea66deb833821cd3124 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 6 May 2017 14:28:44 +0200 Subject: [PATCH 17/24] init -> create --- datastore/keyvalue/clixon_keyvalue.c | 6 +++--- datastore/text/clixon_xmldb_text.c | 8 ++++---- lib/src/clixon_xml_db.c | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/datastore/keyvalue/clixon_keyvalue.c b/datastore/keyvalue/clixon_keyvalue.c index 46e9f0b0..68b2527d 100644 --- a/datastore/keyvalue/clixon_keyvalue.c +++ b/datastore/keyvalue/clixon_keyvalue.c @@ -1552,14 +1552,14 @@ kv_delete(xmldb_handle xh, return retval; } -/*! Initialize database +/*! Create / Initialize database * @param[in] xh XMLDB handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -kv_init(xmldb_handle xh, +kv_create(xmldb_handle xh, char *db) { int retval = -1; @@ -1618,7 +1618,7 @@ static const struct xmldb_api api = { kv_islocked, kv_exists, kv_delete, - kv_init, + kv_create, }; diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 3de5bd2e..d9c86ac5 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -1216,14 +1216,14 @@ text_delete(xmldb_handle xh, return retval; } -/*! Initialize database +/*! Create / init database * @param[in] xh XMLDB handle * @param[in] db Database * @retval 0 OK * @retval -1 Error */ int -text_init(xmldb_handle xh, +text_create(xmldb_handle xh, char *db) { int retval = -1; @@ -1246,7 +1246,7 @@ text_init(xmldb_handle xh, return retval; } -/*! plugin init function */ +/*! plugin exit function */ int text_plugin_exit(void) { @@ -1287,7 +1287,7 @@ static const struct xmldb_api api = { text_islocked, text_exists, text_delete, - text_init, + text_create, }; diff --git a/lib/src/clixon_xml_db.c b/lib/src/clixon_xml_db.c index edddd975..9bc2c2c0 100644 --- a/lib/src/clixon_xml_db.c +++ b/lib/src/clixon_xml_db.c @@ -670,7 +670,7 @@ xmldb_create(clicon_handle h, clicon_err(OE_DB, 0, "No xmldb plugin"); goto done; } - if (xa->xa_init_fn == NULL){ + if (xa->xa_create_fn == NULL){ clicon_err(OE_DB, 0, "No xmldb function"); goto done; } @@ -678,7 +678,7 @@ xmldb_create(clicon_handle h, clicon_err(OE_DB, 0, "Not connected to datastore plugin"); goto done; } - retval = xa->xa_init_fn(xh, db); + retval = xa->xa_create_fn(xh, db); done: return retval; } From 072cdb6bb09b15a56810e69c0fb66792005fb475 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sat, 6 May 2017 14:32:04 +0200 Subject: [PATCH 18/24] restconf dont support patch and put fully --- test/test3.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/test/test3.sh b/test/test3.sh index ed1335e7..45efc73d 100755 --- a/test/test3.sh +++ b/test/test3.sh @@ -56,13 +56,14 @@ $' new "Re-Delete eth0 using none should generate error" expectfn 'curl -sX DELETE http://localhost/restconf/data/interfaces/interface=eth0' "Not Found" -return +if false; then # XXX restconf dont support patch and put fully -new "restconf PATCH config" -expectfn 'curl -sX PATCH -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth4' "" + new "restconf PATCH config" + expectfn 'curl -sX PATCH -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth4' "" -new "restconf PUT" -expectfn 'curl -sX PUT -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth5' "" + new "restconf PUT" + expectfn 'curl -sX PUT -d {"type":"eth"} http://localhost/restconf/data/interfaces/interface=eth5' "" +fi new "Kill restconf daemon" #sudo pkill -u www-data clixon_restconf From 24578767e7f8d2eeb10bf78e99fb32426b193cee Mon Sep 17 00:00:00 2001 From: Olof Hagsand Date: Sun, 7 May 2017 16:14:49 +0200 Subject: [PATCH 19/24] sigpipe problems --- apps/backend/backend_client.c | 16 ++++++++++++++-- apps/backend/clixon_backend_handle.c | 4 ++-- lib/src/clixon_proto.c | 3 +++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index f5e26f8d..ce424606 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -871,9 +871,21 @@ from_client_msg(clicon_handle h, assert(cbuf_len(cbret)); clicon_debug(1, "%s %s", __FUNCTION__, cbuf_get(cbret)); if (send_msg_reply(ce->ce_s, cbuf_get(cbret), cbuf_len(cbret)+1) < 0){ - if (errno == ECONNRESET) + switch (errno){ + case EPIPE: + /* man (2) write: + * EPIPE fd is connected to a pipe or socket whose reading end is + * closed. When this happens the writing process will also receive + * a SIGPIPE signal. + * In Clixon this means a client, eg restconf, netconf or cli closes + * the (UNIX domain) socket. + */ + case ECONNRESET: clicon_log(LOG_WARNING, "client rpc reset"); - goto done; + break; + default: + goto done; + } } // ok: retval = 0; diff --git a/apps/backend/clixon_backend_handle.c b/apps/backend/clixon_backend_handle.c index be2cb07c..14808d1d 100644 --- a/apps/backend/clixon_backend_handle.c +++ b/apps/backend/clixon_backend_handle.c @@ -147,7 +147,7 @@ backend_notify(clicon_handle h, if (strcmp(su->su_stream, stream) == 0){ if (strlen(su->su_filter)==0 || fnmatch(su->su_filter, event, 0) == 0){ if (send_msg_notify(ce->ce_s, level, event) < 0){ - if (errno == ECONNRESET){ + if (errno == ECONNRESET || errno == EPIPE){ clicon_log(LOG_WARNING, "client %d reset", ce->ce_nr); #if 0 /* We should remove here but removal is not possible @@ -225,7 +225,7 @@ backend_notify_xml(clicon_handle h, goto done; } if (send_msg_notify(ce->ce_s, level, cbuf_get(cb)) < 0){ - if (errno == ECONNRESET){ + if (errno == ECONNRESET || errno == EPIPE){ clicon_log(LOG_WARNING, "client %d reset", ce->ce_nr); #if 0 /* We should remove here but removal is not possible diff --git a/lib/src/clixon_proto.c b/lib/src/clixon_proto.c index 172aa696..990e1766 100644 --- a/lib/src/clixon_proto.c +++ b/lib/src/clixon_proto.c @@ -297,8 +297,11 @@ clicon_msg_send(int s, if (atomicio((ssize_t (*)(int, void *, size_t))write, s, msg, ntohs(msg->op_len)) < 0){ clicon_err(OE_CFG, errno, "%s", __FUNCTION__); + clicon_log(LOG_WARNING, "%s: write: %s len:%d msg:%s", __FUNCTION__, + strerror(errno), ntohs(msg->op_len), msg->op_body); goto done; } + ok: retval = 0; done: return retval; From a18f66b6d0fc7e90be88edb05285a18dfade3294 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 7 May 2017 17:38:43 +0200 Subject: [PATCH 20/24] Preparing for 3.3.0 --- CHANGELOG.txt => CHANGELOG.md | 37 ++++----------------- README.md | 53 +++++++++++++++++++----------- apps/backend/backend_client.c | 1 - configure | 2 +- configure.ac | 2 +- datastore/text/clixon_xmldb_text.c | 7 ++-- README.develop => develop.md | 36 +++++--------------- doc/{FAQ.txt => FAQ.md} | 0 example/{README => README.md} | 41 +++++++++++++---------- lib/src/clixon_proto.c | 1 - test/README | 9 ----- test/README.md | 11 +++++++ 12 files changed, 90 insertions(+), 110 deletions(-) rename CHANGELOG.txt => CHANGELOG.md (74%) rename README.develop => develop.md (65%) rename doc/{FAQ.txt => FAQ.md} (100%) rename example/{README => README.md} (86%) delete mode 100644 test/README create mode 100644 test/README.md diff --git a/CHANGELOG.txt b/CHANGELOG.md similarity index 74% rename from CHANGELOG.txt rename to CHANGELOG.md index 84df96ff..0083e82a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.md @@ -1,35 +1,10 @@ -# ***** BEGIN LICENSE BLOCK ***** -# -# Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren -# -# This file is part of CLIXON -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Alternatively, the contents of this file may be used under the terms of -# the GNU General Public License Version 3 or later (the "GPL"), -# in which case the provisions of the GPL are applicable instead -# of those above. If you wish to allow use of your version of this file only -# under the terms of the GPL, and not to allow others to -# use your version of this file under the terms of Apache License version 2, -# indicate your decision by deleting the provisions above and replace them with -# the notice and other provisions required by the GPL. If you do not delete -# the provisions above, a recipient may use your version of this file under -# the terms of any one of the Apache License version 2 or the GPL. -# -# ***** END LICENSE BLOCK ***** +# Clixon CHANGELOG -- Datastore text module is now the default. +## 3.3.0 + +May 2017 + +- Datastore text module is now default. - Refined netconf "none" semantics in tests and text datastore diff --git a/README.md b/README.md index 48780720..afc3f14d 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,21 @@ databases with transaction support. Presentations and tutorial is found on the [CLICON project page](http://www.clicon.org) -## 1. Installation +Table of contents +================= + * [Table of contents](#table-of-contents) + * [Installation](#installation) + * [Documentation](#documentation) + * [Dependencies](#dependencies) + * [Licenses](#licenses) + * [History](#history) + * [Datastore](#datastore) + * [Yang](#yang) + * [Netconf](#netconf) + * [Restconf](#restconf) +Installation +============ A typical installation is as follows: ``` configure # Configure clixon to platform @@ -17,13 +30,15 @@ A typical installation is as follows: ``` One example applications is provided, a IETF IP YANG datamodel with generated CLI and configuration interface. -## 2. Documentation - -- [Frequently asked questions](http://www.clicon.org/FAQ.html) -- [Reference manual(http://www.clicon.org/doxygen/index.html) (may not be 100%% synched) - -## 3. Dependencies +Documentation +============= +- [Frequently asked questions](doc/FAQ.md) +- [Reference manual](http://www.clicon.org/doxygen/index.html) (Better: cd doc; make doc) +- [Routing example](example/README.md) +- [Test](test/README.md) +Dependencies +============ Clixon is dependend on the following packages - [CLIgen](http://www.cligen.se) is required for building CLIXON. If you need to build and install CLIgen: @@ -36,22 +51,23 @@ to build and install CLIgen: - Fcgi (if restconf is enabled) - Qdbm key-value store (if keyvalue datastore is enabled) -## 4. Licenses - +Licenses +======== CLIXON is dual license. Either Apache License, Version 2.0 or GNU General Public License Version 2. You choose. See LICENSE.md for license, CHANGELOG for recent changes. -## 5. History - +History +======= CLIXON is a fork of CLICON where legacy key specification has been replaced completely by YANG. This means that legacy CLICON applications such as CLICON/ROST does not run on CLIXON. Clixon origins from work at [KTH](http://www.csc.kth.se/~olofh/10G_OSR) -## 6. Clixon Datastore +Datastore +========= The Clixon datastore is a stand-alone XML based datastore used by Clixon. The idea is to be able to use different datastores. There is currently a key-value plugin based on qdbm and a plain text-file @@ -141,8 +157,8 @@ datastore_client.c for a more elaborate example. xmdlb_plugin_unload(h); ``` -## 7. YANG - +YANG +==== Clixon implements YANG RFC 6020. Clixon generates an interactive CLI for YANG specifications. It also provides Restconf and Netconf clients. @@ -152,8 +168,8 @@ Clixon YANG currently does not provide the following support: - unique - rpc -## 8. Netconf - +Netconf +======= Clixon Netconf implements the following NETCONF standards: - RFC 4741 (NETCONF Configuration Protocol) - RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) @@ -169,8 +185,8 @@ Clixon NETCONF currently does not support the following Netconf features: - edit-config erropts - edit-config config-text -## 9. Restconf - +Restconf +======== ### Features Clixon restconf is a daemon based on FASTCGI. Instructions are available to @@ -251,4 +267,3 @@ Send command: ``` curl -G http://127.0.0.1/restconf/data/* ``` - diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 6dd5511e..022e61d6 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -621,7 +621,6 @@ from_client_delete_config(clicon_handle h, piddb); goto ok; } - if (xmldb_delete(h, target) < 0){ cprintf(cbret, "" "operation-failed" diff --git a/configure b/configure index 4f7dfcbd..d48a768e 100755 --- a/configure +++ b/configure @@ -2135,7 +2135,7 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu : ${CFLAGS="-O2"} CLIXON_VERSION_MAJOR="3" -CLIXON_VERSION_MINOR="2" +CLIXON_VERSION_MINOR="3" CLIXON_VERSION_PATCH="0" CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}\"" # Fix to specific version (eg 3.5) or head (3) diff --git a/configure.ac b/configure.ac index e5c562bf..c7f30742 100644 --- a/configure.ac +++ b/configure.ac @@ -42,7 +42,7 @@ AC_INIT(lib/clixon/clixon.h.in) : ${CFLAGS="-O2"} CLIXON_VERSION_MAJOR="3" -CLIXON_VERSION_MINOR="2" +CLIXON_VERSION_MINOR="3" CLIXON_VERSION_PATCH="0" CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}\"" # Fix to specific version (eg 3.5) or head (3) diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index d9c86ac5..6ef443e8 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -726,13 +726,14 @@ match_base_child(cxobj *x0, } /*! Modify a base tree x0 with x1 with yang spec y according to operation op - * @param[in] x0 Base xml tree + * @param[in] x0 Base xml tree (can be NULL in add scenarios) + * @param[in] x0p Parent of x0 * @param[in] x1 xml tree which modifies base * @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc - * @param[in] y Yang spec corresponding to xml-node x0. NULL if no x0 + * @param[in] y Yang spec corresponding to xml-node x0. NULL if x0 is NULL + * @param[in] yspec Top-level yang spec (if y is NULL) * Assume x0 and x1 are same on entry and that y is the spec * @see put in clixon_keyvalue.c - * XXX: x1 är det som är under x0 */ static int text_modify(cxobj *x0, diff --git a/README.develop b/develop.md similarity index 65% rename from README.develop rename to develop.md index c260d2a7..a5b07413 100644 --- a/README.develop +++ b/develop.md @@ -1,11 +1,12 @@ -This README contains information for developers: +# README for developers Clixon developers + 1. How to document the code 2. How to work in git (branching) 3. How the meta-configure stuff works -1. How to document the code -+++++++++++++++++++++++++++ +## How to document the code +``` /*! This is a small comment on one line * * This is a detailed description @@ -22,36 +23,17 @@ This README contains information for developers: * @retval FALSE This is a description of another return value * @see See also this function */ +``` +## How to work in git (branching) -2. How to work in git (branching) -+++++++++++++++++++++++++++++++++ Basically follows: http://nvie.com/posts/a-successful-git-branching-model/ only somewhat simplified: Do commits in develop branch. When done, merge with master. -$ git checkout develop -Switch to branch develop -$ git add .. -$ git commit .. -$ git push origin develop -Add/commit stuff here (and push) - -Ready for tagging ------------------ -(This is somewhat simplified - no release branch) -$ ./bump-version.sh 3.6.0 -Files modified successfully, version bumped to 3.6.0 -$ git checkout master -Switch to master -$ git merge --no-ff develop -Merge made by recursive. -(Summary of changes) -$ git tag -a 3.6.0 - -3. How the meta-configure stuff works -+++++++++++++++++++++++++++++++++++++ +## How the meta-configure stuff works +``` configure.ac --. | .------> autoconf* -----> configure [aclocal.m4] --+---+ @@ -64,4 +46,4 @@ configure.ac --. [config.h.in] -. v .-> [config.h] -. +--> config.status* -+ +--> make* Makefile.in ---' `-> Makefile ---' - +``` diff --git a/doc/FAQ.txt b/doc/FAQ.md similarity index 100% rename from doc/FAQ.txt rename to doc/FAQ.md diff --git a/example/README b/example/README.md similarity index 86% rename from example/README rename to example/README.md index e449d864..b03e4cf5 100644 --- a/example/README +++ b/example/README.md @@ -1,8 +1,8 @@ -Clixon yang routing example -+++++++++++++++++++++++++++ +# Clixon yang routing example -0. Compile and run ------------------- + +## Compile and run +``` cd example make && sudo make install # Start backend @@ -11,9 +11,10 @@ clixon_backend -f /usr/local/etc/routing.conf -I clixon_cli -f /usr/local/etc/routing.conf # Send netconf command clixon_netconf -f /usr/local/etc/routing.conf +``` -1. Setting data example using netconf -------------------------------------- +## Setting data example using netconf +``` @@ -28,10 +29,10 @@ clixon_netconf -f /usr/local/etc/routing.conf ]]>]]> +``` -2. Getting data using netconf ------------------------------ - +## Getting data using netconf +``` ]]>]]> ]]>]]> @@ -43,25 +44,29 @@ clixon_netconf -f /usr/local/etc/routing.conf ]]>]]> ]]>]]> +``` + +## Creating notification -3. Creating notification ------------------------- The example has an example notification triggering every 10s. To start a notification stream in the session, create a subscription: +``` ROUTING]]>]]> ]]>]]> Routing notification]]>]]> Routing notification]]>]]> ... - +``` This can also be triggered via the CLI: +``` cli> notify cli> Routing notification Routing notification ... +``` + +## Extending -4. Downcall ------------ Clixon has an extension mechanism which can be used to make extended internal netconf messages to the backend configuration engine. You may need this to make some special operation that is not covered by standard @@ -71,11 +76,13 @@ reference. A more realistic downcall would perform some action, such as reading some status. Example: +``` cli> downcall "This is a string" This is a string -cli>p +``` -5. Run as docker container --------------------------- +## Run as docker container +``` cd docker # look in README +``` \ No newline at end of file diff --git a/lib/src/clixon_proto.c b/lib/src/clixon_proto.c index 990e1766..bd782234 100644 --- a/lib/src/clixon_proto.c +++ b/lib/src/clixon_proto.c @@ -301,7 +301,6 @@ clicon_msg_send(int s, strerror(errno), ntohs(msg->op_len), msg->op_body); goto done; } - ok: retval = 0; done: return retval; diff --git a/test/README b/test/README deleted file mode 100644 index e65ab4c3..00000000 --- a/test/README +++ /dev/null @@ -1,9 +0,0 @@ -This directory contains testing code for clixon and the example -routing application: - clixon A top-level script clones clixon in /tmp and starts all.sh - You can _copy_ this file (review it) and place as cron script - all.sh Run through all tests named 'test*.sh' in this directory. - Therefore, if you place a test in this directory matching - 'test*.sh' it will be run automatically. - test1.sh First test - test2.sh Second test,... diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..4c13fdd1 --- /dev/null +++ b/test/README.md @@ -0,0 +1,11 @@ +# CLixon tests + +This directory contains testing code for clixon and the example +routing application: +- clixon A top-level script clones clixon in /tmp and starts all.sh. You can _copy_ this file (review it) and place as cron script +- all.sh Run through all tests named 'test*.sh' in this directory. Therefore, if you place a test in this directory matching 'test*.sh' it will be run automatically. +- test1.sh CLI tests +- test2.sh Netconf tests +- test3.sh Restconf tests +- test4.sh Yang tests +- test5.sh Datastore tests From b99ce2c499d8949b649fecbdec690b908fbc8f67 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 7 May 2017 18:11:20 +0200 Subject: [PATCH 21/24] docs --- README.md | 233 +++++------------------------------ apps/netconf/README.md | 15 ++- apps/restconf/README.md | 12 +- datastore/README.md | 73 +++++++---- doc/Doxyfile | 2 +- doc/Doxyfile.graphs | 2 +- doc/FAQ.md | 260 +++++++++++++++++++--------------------- example/README.md | 28 ++--- test/README.md | 2 +- 9 files changed, 224 insertions(+), 403 deletions(-) diff --git a/README.md b/README.md index afc3f14d..b199f554 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Clixon is an automatic configuration manager where you from a YANG specification generate interactive CLI, NETCONF, RESTCONF and embedded databases with transaction support. -Presentations and tutorial is found on the [CLICON project page](http://www.clicon.org) +Presentations and tutorial is found on the [Clicon project page](http://www.clicon.org) Table of contents ================= @@ -14,16 +14,13 @@ Table of contents * [Dependencies](#dependencies) * [Licenses](#licenses) * [History](#history) - * [Datastore](#datastore) * [Yang](#yang) - * [Netconf](#netconf) - * [Restconf](#restconf) Installation ============ A typical installation is as follows: ``` - configure # Configure clixon to platform + configure # Configure clixon to platform make # Compile sudo make install # Install libs, binaries, and config-files sudo make install-include # Install include files (for compiling) @@ -33,14 +30,17 @@ One example applications is provided, a IETF IP YANG datamodel with generated CL Documentation ============= - [Frequently asked questions](doc/FAQ.md) +- [XML datastore](datastore/README.md) +- [Netconf support](apps/netconf/README.md) +- [Restconf support](apps/restconf/README.md) - [Reference manual](http://www.clicon.org/doxygen/index.html) (Better: cd doc; make doc) - [Routing example](example/README.md) -- [Test](test/README.md) +- [Tests](test/README.md) Dependencies ============ Clixon is dependend on the following packages -- [CLIgen](http://www.cligen.se) is required for building CLIXON. If you need +- [CLIgen](http://www.cligen.se) is required for building Clixon. If you need to build and install CLIgen: ``` git clone https://github.com/olofhagsand/cligen.git @@ -53,217 +53,38 @@ to build and install CLIgen: Licenses ======== -CLIXON is dual license. Either Apache License, Version 2.0 or GNU +Clixon is dual license. Either Apache License, Version 2.0 or GNU General Public License Version 2. You choose. -See LICENSE.md for license, CHANGELOG for recent changes. +See [LICENSE.md](LICENSE.md) for license, [CHANGELOG](CHANGELOG.md) for recent changes. -History -======= -CLIXON is a fork of CLICON where legacy key specification has been -replaced completely by YANG. This means that legacy CLICON -applications such as CLICON/ROST does not run on CLIXON. +Background +========== -Clixon origins from work at [KTH](http://www.csc.kth.se/~olofh/10G_OSR) - -Datastore -========= -The Clixon datastore is a stand-alone XML based datastore used by -Clixon. The idea is to be able to use different datastores. There is -currently a key-value plugin based on qdbm and a plain text-file -datastore. - -The datastore is primarily designed to be used by Clixon but can be used -separately. - -A datastore is a dynamic plugin that is loaded at runtime with a -well-defined API. This means it is possible to create your own -datastore and plug it in a Clixon backend at runtime. - -### The functional API -``` -int xmldb_plugin_load(clicon_handle h, char *filename); -int xmldb_plugin_unload(clicon_handle h); -int xmldb_connect(clicon_handle h); -int xmldb_disconnect(clicon_handle h); -int xmldb_getopt(clicon_handle h, char *optname, void **value); -int xmldb_setopt(clicon_handle h, char *optname, void *value); -int xmldb_get(clicon_handle h, char *db, char *xpath, - cxobj **xtop, cxobj ***xvec, size_t *xlen); -int xmldb_put(clicon_handle h, char *db, enum operation_type op, - char *api_path, cxobj *xt); -int xmldb_copy(clicon_handle h, char *from, char *to); -int xmldb_lock(clicon_handle h, char *db, int pid); -int xmldb_unlock(clicon_handle h, char *db); -int xmldb_unlock_all(clicon_handle h, int pid); -int xmldb_islocked(clicon_handle h, char *db); -int xmldb_exists(clicon_handle h, char *db); -int xmldb_delete(clicon_handle h, char *db); -int xmldb_create(clicon_handle h, char *db); -``` - -### Using the API - -To use the API, a client needs the following: -- A clicon handle. -- A datastore plugin, such as a text.so or keyvalue.so. These are normally built and installed at Clixon make. -- A directory where to store databases -- A yang specification. This needs to be parsed using the Clixon yang_parse() method. - -A client calling the API needs to (1)load a plugin and (2)connect to a -datastore. You can connect to several datastores, even concurrently, -but in practice in Clixon, you connect to a single store. - -After connecting to a datastore, you can create and modify databases -within the datastore, and set and get options of the datastore itself. - -When done, you disconnect from the datastore and unload the plugin. - -Within a datastore, the following four databases may exist: -- running -- candidate -- startup -- tmp - -Initially, a database does not exist but is created by -xmldb_create(). It is deleted by xmldb_delete(). You may check for -existence with xmldb_exists(). You need to create a database before -you can perform any data access on it. - -You may lock a database for exclusive modification according to -Netconf semantics. You may also unlock a single dabase, unlock all frm -a specific session. - -You can read a database with xmldb_get() and modify a database with -xmldb_put(), and xmldb_copy(). - -A typical datastore session can be as follows, see the source code of -datastore_client.c for a more elaborate example. - -``` - h = clicon_handle_init(); - xmldb_plugin_load(h, plugin); - xmldb_connect(h); - xmldb_setopt(h, "dbdir", dbdir); - xmldb_setopt(h, "yangspec", yspec); - /* From here databases in the datastore may be accessed */ - xmldb_create(h, "candidate"); - xmldb_copy(h, "running", "candidate"); - xmldb_lock(h, "candidate", 7878); - xmldb_put(h, "candidate", OP_CREATE, "/interfaces/interface=eth0", xml); - xmldb_unlock(h, "candidate"); - xmldb_get(h, "candidate", "/", &xml, &xvec, &xlen); - xmldb_disconnect(h) - xmdlb_plugin_unload(h); -``` +We implemented Clixon since we needed a generic configuration tool in +several projects, including +[KTH](http://www.csc.kth.se/~olofh/10G_OSR). Most of these projects +were for embedded network and measuring-probe devices. We started with +something called Clicon which was based on a key-value specification +and data-store. But as time passed new standards evaolved and we +started adapting it to XML, Yang and netconf. Finally we made Clixon +where the legacy key specification has been replaced completely by +YANG and using XML as configuration data. This means that legacy +Clicon applications do not run on Clixon. YANG ==== -Clixon implements YANG RFC 6020. Clixon generates an interactive CLI -for YANG specifications. It also provides Restconf and Netconf clients. -Clixon YANG currently does not provide the following support: +YANG is at the heart of Clixon. RFC 6020 is implemented with some +exceptions as noted below. A Yang specification is used to generated +an interactive CLI client. Clixon also provides a Netconf and Restconf +client based on Yang. + +The following features are (not yet) implemented: - type object-references - if-feature - unique - rpc -Netconf -======= -Clixon Netconf implements the following NETCONF standards: -- RFC 4741 (NETCONF Configuration Protocol) -- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) -- RFC 5277 (NETCONF Event Notifications) -It needs to be updated to RFC6241 and RFC 6242. -Clixon NETCONF currently does not support the following Netconf features: - -- :url capability -- copy-config source config -- edit-config testopts -- edit-config erropts -- edit-config config-text - -Restconf -======== -### Features - -Clixon restconf is a daemon based on FASTCGI. Instructions are available to -run with NGINX. -The implementatation supports plain OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE. -and is based on draft-ietf-netconf-restconf-13. -There is currently (2017) a RFC 8040, many of those features are _not_ implemented, -including: -- query parameters (section 4.9) -- notifications (sec 6) -- only rudimentary error reporting exists (sec 7) - -### Installation using Nginx - -Define nginx config file/etc/nginx/sites-available/default -``` -server { - ... - location /restconf { - root /usr/share/nginx/html/restconf; - fastcgi_pass unix:/www-data/fastcgi_restconf.sock; - include fastcgi_params; - } -} -``` -Start nginx daemon -``` -sudo /etc/init.d nginx start -``` - -Start clixon restconf daemon -``` -olof@vandal> sudo su -c "/www-data/clixon_restconf -f /usr/local/etc/routing.conf " -s /bin/sh www-data -``` - -Make restconf calls with curl -``` -olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces -[ - { - "interfaces": { - "interface":[ - { - "name": "eth0", - "type": "eth", - "enabled": "true", - "name": "eth9", - "type": "eth", - "enabled": "true" - } - ] - } - } -] -olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces/interface/name=eth9/type -[ - { - "type": "eth" - } -] - -curl -sX POST -d '{"clicon":{"interfaces":{"interface":{"name":"eth1","type":"eth","enabled":"true"}}}}' http://localhost/restconf/data -``` - -### Debugging - -Start the restconf fastcgi program with debug flag: -``` -sudo su -c "/www-data/clixon_restconf -Df /usr/local/etc/routing.conf" -s /bin/sh www- -data -``` -Look at syslog: -``` -tail -f /var/log/syslog | grep clixon_restconf -``` - -Send command: -``` -curl -G http://127.0.0.1/restconf/data/* -``` diff --git a/apps/netconf/README.md b/apps/netconf/README.md index 3878aa56..4cd5f454 100644 --- a/apps/netconf/README.md +++ b/apps/netconf/README.md @@ -1,17 +1,16 @@ # Clixon Netconf -Clixon netconf implements the following standards: -- RFC 4741 (NETCONF Configuration Protocol), -- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) and -- RFC 5277 (NETCONF Event Notifications). +Clixon Netconf implements the following NETCONF standards: +- RFC 4741 (NETCONF Configuration Protocol) +- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) +- RFC 5277 (NETCONF Event Notifications) -It needs to be updated to RFC6241 and RFC 6242. It also does not implement the following features: +It needs to be updated to RFC6241 and RFC 6242. + +Clixon NETCONF currently does not support the following features: - :url capability - copy-config source config - edit-config testopts - edit-config erropts - edit-config config-text - - - diff --git a/apps/restconf/README.md b/apps/restconf/README.md index 36916db1..cedf819f 100644 --- a/apps/restconf/README.md +++ b/apps/restconf/README.md @@ -1,11 +1,5 @@ # Clixon Restconf - -Contents: -1. Features -2. Installation using NGINX -3. Debugging - -## 1. FEATURES +### Features Clixon restconf is a daemon based on FASTCGI. Instructions are available to run with NGINX. @@ -17,7 +11,7 @@ including: - notifications (sec 6) - only rudimentary error reporting exists (sec 7) -## 2. INSTALLATION using NGINX +### Installation using Nginx Define nginx config file/etc/nginx/sites-available/default ``` @@ -69,7 +63,7 @@ olof@vandal> curl -G http://127.0.0.1/restconf/data/interfaces/interface/name=et curl -sX POST -d '{"clicon":{"interfaces":{"interface":{"name":"eth1","type":"eth","enabled":"true"}}}}' http://localhost/restconf/data ``` -## DEBUGGING +### Debugging Start the restconf fastcgi program with debug flag: ``` diff --git a/datastore/README.md b/datastore/README.md index f9baa58b..774e3a92 100644 --- a/datastore/README.md +++ b/datastore/README.md @@ -2,14 +2,17 @@ The Clixon datastore is a stand-alone XML based datastore used by Clixon. The idea is to be able to use different datastores. There is -currently a Key-value plugin based on qdbm and a plain text-file +currently a key-value plugin based on qdbm and a plain text-file datastore. The datastore is primarily designed to be used by Clixon but can be used -separately. See datastore_client.c for an example of how to use a -datastore plugin for other applications +separately. -## The functional API +A datastore is a dynamic plugin that is loaded at runtime with a +well-defined API. This means it is possible to create your own +datastore and plug it in a Clixon backend at runtime. + +### The functional API ``` int xmldb_plugin_load(clicon_handle h, char *filename); int xmldb_plugin_unload(clicon_handle h); @@ -28,40 +31,62 @@ int xmldb_unlock_all(clicon_handle h, int pid); int xmldb_islocked(clicon_handle h, char *db); int xmldb_exists(clicon_handle h, char *db); int xmldb_delete(clicon_handle h, char *db); -int xmldb_init(clicon_handle h, char *db); +int xmldb_create(clicon_handle h, char *db); ``` -## Using the API +### Using the API -This section described how the API is used. Please refer to datastore/datastore_client.c for an example of an actual implementation. - -### Prerequisities To use the API, a client needs the following: - A clicon handle. - A datastore plugin, such as a text.so or keyvalue.so. These are normally built and installed at Clixon make. -- A directory where to store the datastores +- A directory where to store databases - A yang specification. This needs to be parsed using the Clixon yang_parse() method. -### Dynamics +A client calling the API needs to (1)load a plugin and (2)connect to a +datastore. You can connect to several datastores, even concurrently, +but in practice in Clixon, you connect to a single store. -A client calling the API needs to load a plugin and connect to a -datastore. In principle, you cannot connect to several datastores, -even concurrently, but in practice in Clixon, you connect to a single -store. +After connecting to a datastore, you can create and modify databases +within the datastore, and set and get options of the datastore itself. -Within a datastore, there may be +When done, you disconnect from the datastore and unload the plugin. + +Within a datastore, the following four databases may exist: +- running +- candidate +- startup +- tmp + +Initially, a database does not exist but is created by +xmldb_create(). It is deleted by xmldb_delete(). You may check for +existence with xmldb_exists(). You need to create a database before +you can perform any data access on it. + +You may lock a database for exclusive modification according to +Netconf semantics. You may also unlock a single dabase, unlock all frm +a specific session. + +You can read a database with xmldb_get() and modify a database with +xmldb_put(), and xmldb_copy(). + +A typical datastore session can be as follows, see the source code of +datastore_client.c for a more elaborate example. ``` h = clicon_handle_init(); xmldb_plugin_load(h, plugin); -``` -The plugin is the complete path-name of a file. - -Thereafter, a connection is made to a specific plugin, such as a text plugin. It is possible to connect to several datastore at once, although this is not supported by CLixon: -``` -xmldb_connect(h); -xmldb_setopt(h, "dbdir", dbdir); -xmldb_setopt(h, "yangspec", yspec); + xmldb_connect(h); + xmldb_setopt(h, "dbdir", dbdir); + xmldb_setopt(h, "yangspec", yspec); + /* From here databases in the datastore may be accessed */ + xmldb_create(h, "candidate"); + xmldb_copy(h, "running", "candidate"); + xmldb_lock(h, "candidate", 7878); + xmldb_put(h, "candidate", OP_CREATE, "/interfaces/interface=eth0", xml); + xmldb_unlock(h, "candidate"); + xmldb_get(h, "candidate", "/", &xml, &xvec, &xlen); + xmldb_disconnect(h) + xmdlb_plugin_unload(h); ``` diff --git a/doc/Doxyfile b/doc/Doxyfile index f18d01f3..98375856 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "CliXoN" +PROJECT_NAME = "clixon" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/doc/Doxyfile.graphs b/doc/Doxyfile.graphs index 7a115a20..03ca9505 100644 --- a/doc/Doxyfile.graphs +++ b/doc/Doxyfile.graphs @@ -32,7 +32,7 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = "CLICON" +PROJECT_NAME = "clixon" # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version diff --git a/doc/FAQ.md b/doc/FAQ.md index 79c38315..f11b497d 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -1,112 +1,66 @@ -Frequently Asked Questions - CliXon -=================================== +# Clixon FAQ -Q: What is CliXon? ------------------- -CliXon is a configuration management tool including CLI generation, -Yang parser, netconf interface and an embedded databases. +## What is Clixon? -Q: Why should you use CliXon? ------------------------------ -If you want an easy-to-use config frontend based on yang with an open-source license. -Typically for embedded devices requiring a config interface such as routers and switches. +Clixon is a configuration management tool including a generated CLI , +Yang parser, netconf and restconf interface and an embedded databases. -Q: What license is available? ------------------------------ -The basic license is open-source, GPLv3. Contact authors for commercial license. +## Why should I use Clixon? -Q: Is CliXon extendible? ------------------------- -Yes. All application semantics is defined in plugins with well-defined APIs. There are currently three types of plugins: for CLI, for Netconf and for the backend. +If you want an easy-to-use configuration frontend based on yang with an +open-source license. Typically for embedded devices requiring a +config interface such as routers and switches. -Q: Which language is CliXon implemented in? -------------------------------------------- -CliXon is written in C. The plugins are written in C. The CLI +## What license is available? +CLIXON is dual license. Either Apache License, Version 2.0 or GNU +General Public License Version 2. + +## Is Clixon extendible? +Yes. All application semantics is defined in plugins with well-defined +APIs. There are currently plugins for: CLI, Netconf, Restconf, the datastore and the backend. + +## Which programming language is used? +Clixon is written in C. The plugins are written in C. The CLI specification uses cligen (http://cligen.se) There is a project for writing plugins in Python. It is reasonable simple to spawn an external script from a backend. -Q: Is CliXon different from Clicon? ------------------------------------ -CliXon is a fork of Clicon focussing on Yang specification and XML -database. The yang support in clicon was grown out of a key-based -scheme that became more and more complex since it had to translate -between many different formats. CliXon uses only XML internally. +## How to best understand Clixon? +Run the ietf yang routing example, in the example directory. -The commit transaction mechanism has been simplified too. But CliXon -is not backward compliant with key-based Clixon applications, such as -Rost. - -Q: How to best understand CliXon? ---------------------------------- -Run the ietf yang routing example. It is used in many of the answers below. - -Q: How do you build and install CliXon (and the example)? ---------------------------------------------------------- -CliXon: +## How do you build and install Clixon (and the example)? +Clixon: +``` ./configure; make; sudo make install; sudo make install-include - +``` The example: +``` cd example; make; sudo make install +``` -Q: What about reference documentation? --------------------------------------- -CliXon uses Doxygen for reference documentation. +## What about reference documentation? +Clixon uses Doxygen for reference documentation. Build using 'make doc' and aim your browser at doc/html/index.html or use the web resource: http://clicon.org/ref/index.html -Q: How do you run the example? ------------------------------- +## How do you run the example? - Start a backend server: 'clixon_backend -Ff /usr/local/etc/routing.conf' - Start a cli session: clixon_cli -f /usr/local/etc/routing.conf - Start a netconf session: clixon_netconf -f /usr/local/etc/routing.conf -Q: Can you run CliXon as docker containers? -------------------------------------------- -Yes, the example works as docker containers as well. backend and cli needs a -common file-system so they need to run as a composed pair. - cd example/docker - make docker # Prepares /data as shared file-system mount - run.sh # Starts an example backend and a cli +## How is configuration data stored? +Configuration data is stored in an XML datastore. The default is a +text-based addatastore, but there also exists a key-value datastore +using qdbm. In the example the datastore are regular files found in +/usr/local/var/routing/. -The containers are by default downloaded from dockerhib, but you may -build the containers locally: - cd docker - make docker - -You may also push the containers with 'make push' but you may then consider changing the image name in the makefile. - -Q: How do you change the example? ---------------------------------- -- routing.conf.local - Override default settings -- The yang specifications - This is the central part. It changes the XML, database and the config cli. -- routing_cli.cli - Change the fixed part of the CLI commands -- routing_cli.c - Cli C-commands are placed here. -- routing_backend.c - Commit and validate functions. -- routing_netconf.c - Modify semantics of netconf commands. - - -Q: How do you check what is in a database? ------------------------------------------- -Use clixon_dbctrl. The name of the running or candidate databases are found in the -configuration file. -Example: - > clixon_dbctrl -d /usr/local/var/routing/candidate_db / -p - /interfaces/interface/eth0/ipv4 - /interfaces/interface/eth0/type bgp - /interfaces/interface/eth0 - /interfaces/interface/eth0/name eth0 -Each line corresponds to a database entry (node in an XML tree). -If the node is a leaf, the value appears as the second entry ('bgp' and 'eth0'). - -Q: What is validate and commit? -------------------------------- +## What is validate and commit? Clixon follows netconf in its validate and commit semantics. In short, you edit a 'candidate' configuration, which is first 'validated' for consistency and then 'committed' to the 'running' @@ -116,59 +70,31 @@ A clixon developer writes commit functions to incrementaly upgrade a system state based on configuration changes. Writing commit callbacks is the core functionality of a clixon system. -Q: How do you write a commit function? --------------------------------------- -You write a commit function in routing_backend.c. -Every time a commit is made, transaction_commit() is called in the -backend. It has a 'transaction_data td' argument which is used to fetch -information on added, deleted and changed entries. You access this -information using access functions as defined in clixon_backend_transaction.h +## What is a Clixon configuration file? +Clixon options are stored in a configuration file you must specify +when you start a backend or client using -f. The example configuration +file is /usr/local/etc/routing.conf. +This file is generated from the base source clixon.conf.cpp.cpp and +is merged with local configuration files, such as routing.conf.local. +This is slightly confusing and could be improved. -Q: How do you check what has changed on commit? ------------------------------------------------ -You use XPATHs on the XML trees in the transaction commit callback. -Suppose you want to print all added interfaces: - cxobj *target = transaction_target(td); # wanted XML tree - vec = xpath_vec_flag(target, "//interface", &len, XML_FLAG_ADD); /* Get added i/fs */ - for (i=0; i("This is a variable"), mycallback("myarg"); -(2) Then define a function in routing_cli.c - mycallback(clicon_handle h, cvec *cvv, cg_var *arg) -where 'cvv' contains the value of the variable and 'arg' contains the -function parameter 'myarg'. - -Q: What are cg_var and cvec used in CLI callbacks? --------------------------------------------------- -Those are 'CLIgen variables' and vector of CLIgen variables. -They are documented in CLIgen documentation. Some examples on usage is found in the -routing_cli.c - -Q: How do you write a validation function? ------------------------------------------- -Similar to a commit function, but instead write the transaction_validate() function. -Check for inconsistencies in the XML trees and if they fail, make an clicon_err() call. - clicon_err(OE_PLUGIN, 0, "Route %s lacks ipv4 addr", name); - return -1; -The validation or commit will then be aborted. - -Q: How do you use netconf? --------------------------- +## How do I use netconf? As an alternative to cli configuration, you can use netconf. Easiest is to just pipe netconf commands to the clixon_netconf application. Example: @@ -176,25 +102,81 @@ Example: However, more useful is to run clixon_netconf as an SSH subsystem. Register the subsystem in /etc/sshd_config: - +``` Subsystem netconf /usr/local/bin/clixon_netconf - +``` and then invoke it from a client using +``` ssh -s netconf +``` + +## How do I use notifications? -Q: How do you use notifications? --------------------------------- The example has a prebuilt notification stream called "ROUTING" that triggers every 10s. You enable the notification either via the cli or via netconf: cli> notify cli> Routing notification Routing notification -... - -> clixon_netconf -qf /usr/local/etc/routing.conf +``` +clixon_netconf -qf /usr/local/etc/routing.conf ROUTING]]>]]> ]]>]]> Routing notification]]>]]> Routing notification]]>]]> ... +## I want to program. How do I extend the example? +- routing.conf.local - Override default settings +- The yang specifications - This is the central part. It changes the XML, database and the config cli. +- routing_cli.cli - Change the fixed part of the CLI commands +- routing_cli.c - Cli C-commands are placed here. +- routing_backend.c - Commit and validate functions. +- routing_netconf.c - Modify semantics of netconf commands. + +## How do I write a commit function? +You write a commit function in routing_backend.c. +Every time a commit is made, transaction_commit() is called in the +backend. It has a 'transaction_data td' argument which is used to fetch +information on added, deleted and changed entries. You access this +information using access functions as defined in clixon_backend_transaction.h + +## How do i check what has changed on commit? +You use XPATHs on the XML trees in the transaction commit callback. +Suppose you want to print all added interfaces: +``` + cxobj *target = transaction_target(td); # wanted XML tree + vec = xpath_vec_flag(target, "//interface", &len, XML_FLAG_ADD); /* Get added i/fs */ + for (i=0; i example("This is a comment") ("This is a variable"), mycallback("myarg"); +2. Then define a function in routing_cli.c +> mycallback(clicon_handle h, cvec *cvv, cvec *arv) +where 'cvv' contains the value of the variable 'var' and 'argv' contains the string "myarg". + +The 'cvv' datatype is a 'CLIgen variable vector'. +They are documented in [CLIgen tutorial](https://github.com/olofhagsand/cligen/blob/master/cligen_tutorial.pdf) + +## How do I write a validation function? +Similar to a commit function, but instead write the transaction_validate() function. +Check for inconsistencies in the XML trees and if they fail, make an clicon_err() call. + clicon_err(OE_PLUGIN, 0, "Route %s lacks ipv4 addr", name); + return -1; +The validation or commit will then be aborted. + diff --git a/example/README.md b/example/README.md index b03e4cf5..8c325b60 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,21 @@ # Clixon yang routing example - ## Compile and run ``` -cd example -make && sudo make install -# Start backend -clixon_backend -f /usr/local/etc/routing.conf -I -# Edit cli -clixon_cli -f /usr/local/etc/routing.conf -# Send netconf command -clixon_netconf -f /usr/local/etc/routing.conf + cd example + make && sudo make install +``` +Start backend: +``` + clixon_backend -f /usr/local/etc/routing.conf -I +``` +Edit cli: +``` + clixon_cli -f /usr/local/etc/routing.conf +``` +Send netconf command: +``` + clixon_netconf -f /usr/local/etc/routing.conf ``` ## Setting data example using netconf @@ -34,15 +39,10 @@ clixon_netconf -f /usr/local/etc/routing.conf ## Getting data using netconf ``` ]]>]]> - ]]>]]> - ]]>]]> - ]]>]]> - ]]>]]> - ]]>]]> ``` diff --git a/test/README.md b/test/README.md index 4c13fdd1..2267552d 100644 --- a/test/README.md +++ b/test/README.md @@ -1,4 +1,4 @@ -# CLixon tests +# Clixon tests This directory contains testing code for clixon and the example routing application: From e07de7414bf585d6e68743cfeffde69bc1ee0304 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 7 May 2017 18:50:38 +0200 Subject: [PATCH 22/24] docs --- README.doxygen | 31 -------------------- README.md | 56 +++++++++++++++++++------------------ apps/netconf/README.md | 10 +++---- apps/restconf/README.md | 2 +- doc/clixon_example_sdk.png | Bin 0 -> 132657 bytes 5 files changed, 35 insertions(+), 64 deletions(-) delete mode 100644 README.doxygen create mode 100644 doc/clixon_example_sdk.png diff --git a/README.doxygen b/README.doxygen deleted file mode 100644 index 9ad563b4..00000000 --- a/README.doxygen +++ /dev/null @@ -1,31 +0,0 @@ -/*! This is a small comment on one line - * - * This is a detailed description - * spanning several lines. - * - * Example usage: - * @code - * fn(a, &b); - * @endcode - * - * @param[in] src This is a description of the first parameter - * @param[in,out] dest This is a description of the second parameter - * @retval TRUE This is a description of the return value - * @retval FALSE This is a description of another return value - * @see anotherfn() - * @note This is just an example - */ - -/*! This is a documentation of a typedef or struct - * - * This is a detailed description - * spanning several lines. - * - * Example usage: - * @code - * struct foo f; - * @endcode - */ -typedef foo{ - int a; /**< This is the a field*/ -}; diff --git a/README.md b/README.md index b199f554..4712f574 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,30 @@ # Clixon -Clixon is an automatic configuration manager where you from a YANG -specification generate interactive CLI, NETCONF, RESTCONF and embedded -databases with transaction support. +Clixon is an automatic configuration manager where you generate +interactive CLI, NETCONF, RESTCONF and embedded databases with +transaction support from a YANG specification. -Presentations and tutorial is found on the [Clicon project page](http://www.clicon.org) +![clixon sdk](doc/clixon_example_sdk.png) Table of contents ================= - * [Table of contents](#table-of-contents) - * [Installation](#installation) * [Documentation](#documentation) + * [Installation](#installation) * [Dependencies](#dependencies) * [Licenses](#licenses) - * [History](#history) - * [Yang](#yang) + * [Background](#background) + * [Yang and XML](#yang-and-xml) + +Documentation +============= +- [Frequently asked questions](doc/FAQ.md) +- [XML datastore](datastore/README.md) +- [Netconf support](apps/netconf/README.md) +- [Restconf support](apps/restconf/README.md) +- [Reference manual](http://www.clicon.org/doxygen/index.html) (Better: cd doc; make doc) +- [Routing example](example/README.md) +- [Clicon project page](http://www.clicon.org) +- [Tests](test/README.md) Installation ============ @@ -25,17 +35,9 @@ A typical installation is as follows: sudo make install # Install libs, binaries, and config-files sudo make install-include # Install include files (for compiling) ``` -One example applications is provided, a IETF IP YANG datamodel with generated CLI and configuration interface. -Documentation -============= -- [Frequently asked questions](doc/FAQ.md) -- [XML datastore](datastore/README.md) -- [Netconf support](apps/netconf/README.md) -- [Restconf support](apps/restconf/README.md) -- [Reference manual](http://www.clicon.org/doxygen/index.html) (Better: cd doc; make doc) -- [Routing example](example/README.md) -- [Tests](test/README.md) +One [example application](example/README.md) is provided, a IETF IP YANG datamodel with +generated CLI and configuration interface. Dependencies ============ @@ -60,7 +62,6 @@ See [LICENSE.md](LICENSE.md) for license, [CHANGELOG](CHANGELOG.md) for recent c Background ========== - We implemented Clixon since we needed a generic configuration tool in several projects, including [KTH](http://www.csc.kth.se/~olofh/10G_OSR). Most of these projects @@ -72,16 +73,17 @@ where the legacy key specification has been replaced completely by YANG and using XML as configuration data. This means that legacy Clicon applications do not run on Clixon. -YANG -==== +YANG and XML +============ -YANG is at the heart of Clixon. RFC 6020 is implemented with some -exceptions as noted below. A Yang specification is used to generated -an interactive CLI client. Clixon also provides a Netconf and Restconf -client based on Yang. +YANG and XML is at the heart of Clixon. Yang modules are used as a +specification for handling XML configuration data. The spec is also +used to generate an interactive CLI client as well as provide +[Netconf](apps/netconf/README.md) and +[Restconf](apps/restconf/README.md) clients. -The following features are (not yet) implemented: -- type object-references +The [YANG RFC 6020](https://www.rfc-editor.org/rfc/rfc6020.txt) is implemented with the following exceptions: +- object-references - if-feature - unique - rpc diff --git a/apps/netconf/README.md b/apps/netconf/README.md index 4cd5f454..92e6a8b9 100644 --- a/apps/netconf/README.md +++ b/apps/netconf/README.md @@ -1,11 +1,11 @@ # Clixon Netconf -Clixon Netconf implements the following NETCONF standards: -- RFC 4741 (NETCONF Configuration Protocol) -- RFC 4742 (Using the NETCONF Configuration Protocol over Secure SHell (SSH)) -- RFC 5277 (NETCONF Event Notifications) +Clixon Netconf implements the following NETCONF proposals or standards: +- [NETCONF Configuration Protocol](http://www.rfc-base.org/txt/rfc-4741.txt) +- [Using the NETCONF Configuration Protocol over Secure SHell (SSH)](http://www.rfc-base.org/txt/rfc-4742.txt) +- [NETCONF Event Notifications](http://www.rfc-base.org/txt/rfc-5277.txt) -It needs to be updated to RFC6241 and RFC 6242. +However, it needs to be updated to RFC 6241 and RFC 6242. Clixon NETCONF currently does not support the following features: diff --git a/apps/restconf/README.md b/apps/restconf/README.md index cedf819f..81905802 100644 --- a/apps/restconf/README.md +++ b/apps/restconf/README.md @@ -5,7 +5,7 @@ Clixon restconf is a daemon based on FASTCGI. Instructions are available to run with NGINX. The implementatation supports plain OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE. and is based on draft-ietf-netconf-restconf-13. -There is currently (2017) a RFC 8040, many of those features are _not_ implemented, +There is currently (2017) a [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040), many of those features are _not_ implemented, including: - query parameters (section 4.9) - notifications (sec 6) diff --git a/doc/clixon_example_sdk.png b/doc/clixon_example_sdk.png new file mode 100644 index 0000000000000000000000000000000000000000..ca5f91e1eda1b045e799a5d2cc60ed34dd9e9e61 GIT binary patch literal 132657 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

Feq3o`cqU$#BJnaua~ZHc}f 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

w)% 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 G&;$Tpgy`A; literal 0 HcmV?d00001 From 74f1728619def7c371cb47f66966158421d0f979 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 7 May 2017 19:03:10 +0200 Subject: [PATCH 23/24] docs --- README.md | 20 ++++++++++++-------- datastore/README.md | 16 +++++++++------- doc/FAQ.md | 1 + test/README.md | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4712f574..a85e8c55 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,6 @@ Clixon is an automatic configuration manager where you generate interactive CLI, NETCONF, RESTCONF and embedded databases with transaction support from a YANG specification. -![clixon sdk](doc/clixon_example_sdk.png) - Table of contents ================= * [Documentation](#documentation) @@ -13,7 +11,7 @@ Table of contents * [Dependencies](#dependencies) * [Licenses](#licenses) * [Background](#background) - * [Yang and XML](#yang-and-xml) + * [Clixon SDK](#SDK) Documentation ============= @@ -21,9 +19,9 @@ Documentation - [XML datastore](datastore/README.md) - [Netconf support](apps/netconf/README.md) - [Restconf support](apps/restconf/README.md) -- [Reference manual](http://www.clicon.org/doxygen/index.html) (Better: cd doc; make doc) +- [Reference manual](http://www.clicon.org/doxygen/index.html) (Note the link may not be up-to-date. It is better to build your own: cd doc; make doc) - [Routing example](example/README.md) -- [Clicon project page](http://www.clicon.org) +- [Clicon and Clixon project page](http://www.clicon.org) - [Tests](test/README.md) Installation @@ -41,7 +39,7 @@ generated CLI and configuration interface. Dependencies ============ -Clixon is dependend on the following packages +Clixon is dependend on the following software packages, which need to exist on the target machine. - [CLIgen](http://www.cligen.se) is required for building Clixon. If you need to build and install CLIgen: ``` @@ -53,6 +51,8 @@ to build and install CLIgen: - Fcgi (if restconf is enabled) - Qdbm key-value store (if keyvalue datastore is enabled) +There is no yum/apt/ostree package for Clixon yet (please help?) + Licenses ======== Clixon is dual license. Either Apache License, Version 2.0 or GNU @@ -73,8 +73,12 @@ where the legacy key specification has been replaced completely by YANG and using XML as configuration data. This means that legacy Clicon applications do not run on Clixon. -YANG and XML -============ +SDK +=== + +clixon sdk + +The figure shows the SDK runtime of Clixon. YANG and XML is at the heart of Clixon. Yang modules are used as a specification for handling XML configuration data. The spec is also diff --git a/datastore/README.md b/datastore/README.md index 774e3a92..a3283c6e 100644 --- a/datastore/README.md +++ b/datastore/README.md @@ -1,9 +1,9 @@ # Clixon datastore -The Clixon datastore is a stand-alone XML based datastore used by -Clixon. The idea is to be able to use different datastores. There is -currently a key-value plugin based on qdbm and a plain text-file -datastore. +The Clixon datastore is a stand-alone XML based datastore. The idea is +to be able to use different datastores backends with the same +API. There is currently a key-value plugin based on qdbm and a plain +text-file datastore. The datastore is primarily designed to be used by Clixon but can be used separately. @@ -42,8 +42,10 @@ To use the API, a client needs the following: - A directory where to store databases - A yang specification. This needs to be parsed using the Clixon yang_parse() method. -A client calling the API needs to (1)load a plugin and (2)connect to a -datastore. You can connect to several datastores, even concurrently, +A client calling the API needs to: +1. Load a plugin and +2. Connect to a datastore. +You can connect to several datastores, even concurrently, but in practice in Clixon, you connect to a single store. After connecting to a datastore, you can create and modify databases @@ -70,7 +72,7 @@ You can read a database with xmldb_get() and modify a database with xmldb_put(), and xmldb_copy(). A typical datastore session can be as follows, see the source code of -datastore_client.c for a more elaborate example. +[datastore_client.c](datastore_client.c) for a more elaborate example. ``` h = clicon_handle_init(); diff --git a/doc/FAQ.md b/doc/FAQ.md index f11b497d..8a57496f 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -124,6 +124,7 @@ clixon_netconf -qf /usr/local/etc/routing.conf Routing notification]]>]]> Routing notification]]>]]> ... +``` ## I want to program. How do I extend the example? - routing.conf.local - Override default settings diff --git a/test/README.md b/test/README.md index 2267552d..d58863c2 100644 --- a/test/README.md +++ b/test/README.md @@ -2,7 +2,7 @@ This directory contains testing code for clixon and the example routing application: -- clixon A top-level script clones clixon in /tmp and starts all.sh. You can _copy_ this file (review it) and place as cron script +- clixon A top-level script clones clixon in /tmp and starts all.sh. You can copy this file (review it first) and place as cron script - all.sh Run through all tests named 'test*.sh' in this directory. Therefore, if you place a test in this directory matching 'test*.sh' it will be run automatically. - test1.sh CLI tests - test2.sh Netconf tests From 2893bb9b418e4b2b72291c5d62ced5e6804ae345 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 7 May 2017 19:05:01 +0200 Subject: [PATCH 24/24] docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a85e8c55..ec7ad802 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ to build and install CLIgen: - Fcgi (if restconf is enabled) - Qdbm key-value store (if keyvalue datastore is enabled) -There is no yum/apt/ostree package for Clixon yet (please help?) +There is no yum/apt/ostree package for Clixon (please help?) Licenses ======== @@ -76,7 +76,7 @@ Clicon applications do not run on Clixon. SDK === -clixon sdk +clixon sdk The figure shows the SDK runtime of Clixon.