diff --git a/CHANGELOG.md b/CHANGELOG.md index 57be6bf7..63c5c63f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ ### API changes on existing features (you may need to change your code) +* Non-key list now not accepted in edit-config (before only on validation) * Restconf with startup feature will now copy all edit changes to startup db (as it should according to RFC 8040) * Netconf Startup feature is no longer hardcoded, you need to explicitly enable it (See RFC 6241, Section 8.7) * Enable in config file with: `ietf-netconf:startup`, or use `*:*` diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 358d4c5e..73361f8d 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -433,6 +433,7 @@ from_client_edit_config(clicon_handle h, */ if (xml_apply(xc, CX_ELMNT, xml_spec_populate, yspec) < 0) goto done; + if (xml_apply(xc, CX_ELMNT, xml_non_config_data, &non_config) < 0) goto done; if (non_config){ @@ -440,6 +441,11 @@ from_client_edit_config(clicon_handle h, goto done; goto ok; } + /* xmldb_put (difflist handling) requires list keys */ + if ((ret = xml_yang_validate_list_key_only(xc, cbret)) < 0) + goto done; + if (ret == 0) + goto ok; /* Cant do this earlier since we dont have a yang spec to * the upper part of the tree, until we get the "config" tree. */ diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c index 3565ea83..6587b581 100644 --- a/apps/backend/backend_commit.c +++ b/apps/backend/backend_commit.c @@ -160,6 +160,7 @@ generic_validate(yang_stmt *yspec, * 4. Validate startup db. (valid) * 5. If valid fails, call startup-cb(Invalid, msdiff), keep startup in candidate and commit failsafe db. Done. * 6. Call startup-cb(OK, msdiff) and commit. + * @see from_validate_common for incoming validate/commit */ static int startup_common(clicon_handle h, @@ -181,10 +182,14 @@ startup_common(clicon_handle h, if ((msd = modstate_diff_new()) == NULL) goto done; clicon_debug(1, "Reading startup config from %s", db); - if (xmldb_get(h, db, "/", &xt, msd) < 0) + if (xmldb_get1(h, db, "/", &xt, msd) < 0) goto done; + /* Clear flags xpath for get */ + xml_apply0(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, + (void*)(XML_FLAG_MARK|XML_FLAG_CHANGE)); if (xml_child_nr(xt) == 0){ /* If empty skip */ td->td_target = xt; + xt = NULL; goto ok; } if (msd){ @@ -204,6 +209,7 @@ startup_common(clicon_handle h, goto done; /* Handcraft transition with with only add tree */ td->td_target = xt; + xt = NULL; x = NULL; while ((x = xml_child_each(td->td_target, x, CX_ELMNT)) != NULL){ if (cxvec_append(x, &td->td_avec, &td->td_alen) < 0) @@ -232,6 +238,8 @@ startup_common(clicon_handle h, ok: retval = 1; done: + if (xt) + xml_free(xt); if (msd) modstate_diff_free(msd); return retval; @@ -267,14 +275,23 @@ startup_validate(clicon_handle h, goto done; if (ret == 0) goto fail; + /* Clear cached trees from default values and marking */ + if (xmldb_get1_clear(h, db) < 0) + goto done; if (xtr){ *xtr = td->td_target; td->td_target = NULL; } retval = 1; done: - if (td) - transaction_free(td); + if (td){ + if (clicon_option_bool(h, "CLICON_XMLDB_CACHE")){ + /* xmldb_get1 requires free only if not cache */ + td->td_target = NULL; + td->td_src = NULL; + } + transaction_free(td); + } return retval; fail: /* cbret should be set */ retval = 0; @@ -314,6 +331,10 @@ startup_commit(clicon_handle h, /* 8. Call plugin transaction commit callbacks */ if (plugin_transaction_commit(h, td) < 0) goto done; + /* Clear cached trees from default values and marking */ + if (xmldb_get1_clear(h, db) < 0) + goto done; + /* [Delete and] create running db */ if (xmldb_exists(h, "running") == 1){ if (xmldb_delete(h, "running") != 0 && errno != ENOENT) @@ -334,8 +355,14 @@ startup_commit(clicon_handle h, retval = 1; done: - if (td) - transaction_free(td); + if (td){ + if (clicon_option_bool(h, "CLICON_XMLDB_CACHE")){ + /* xmldb_get1 requires free only if not cache */ + td->td_target = NULL; + td->td_src = NULL; + } + transaction_free(td); + } return retval; fail: /* cbret should be set */ retval = 0; @@ -352,6 +379,7 @@ startup_commit(clicon_handle h, * @retval 1 Validation OK * @note Need to differentiate between error and validation fail * (only done for generic_validate) + * @see startup_common for startup scenario */ static int from_validate_common(clicon_handle h, @@ -511,6 +539,16 @@ candidate_commit(clicon_handle h, */ if (xmldb_copy(h, candidate, "running") < 0) goto done; + /* Here pointers to old (source) tree are obsolete */ + if (td->td_dvec){ + td->td_dlen = 0; + free(td->td_dvec); + td->td_dvec = NULL; + } + if (td->td_scvec){ + free(td->td_scvec); + td->td_scvec = NULL; + } /* 9. Call plugin transaction end callbacks */ plugin_transaction_end(h, td); diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index e132435e..9e663cba 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -709,7 +709,8 @@ main(int argc, demonized errors OK. Before this stage, errors are logged on stderr also */ if (foreground==0){ - clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, CLICON_LOG_SYSLOG); + clicon_log_init(__PROGRAM__, debug?LOG_DEBUG:LOG_INFO, + logdst==CLICON_LOG_FILE?CLICON_LOG_FILE:CLICON_LOG_SYSLOG); if (daemon(0, 0) < 0){ fprintf(stderr, "config: daemon"); exit(-1); diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index 367ed627..96195f31 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -262,11 +262,6 @@ startup_extraxml(clicon_handle h, goto fail; if (xt==NULL || xml_child_nr(xt)==0) goto ok; - /* Write (potentially modified) xml tree xt back to tmp - */ - if ((ret = xmldb_put(h, "tmp", OP_REPLACE, xt, - clicon_username_get(h), cbret)) < 0) - goto done; /* Merge tmp into running (no commit) */ if ((ret = db_merge(h, db, "running", cbret)) < 0) goto fail; @@ -275,9 +270,9 @@ startup_extraxml(clicon_handle h, ok: retval = 1; done: - if (xt) + if (xt && !clicon_option_bool(h, "CLICON_XMLDB_CACHE")) xml_free(xt); - if (xmldb_delete(h, "tmp") != 0 && errno != ENOENT) + if (xmldb_delete(h, db) != 0 && errno != ENOENT) return -1; return retval; fail: diff --git a/apps/backend/clixon_backend_transaction.c b/apps/backend/clixon_backend_transaction.c index 727a7332..46d4f827 100644 --- a/apps/backend/clixon_backend_transaction.c +++ b/apps/backend/clixon_backend_transaction.c @@ -188,6 +188,9 @@ transaction_clen(transaction_data td) return ((transaction_data_t *)td)->td_clen; } +/*! Print transaction on FILE for debug + * @see transaction_log + */ int transaction_print(FILE *f, transaction_data th) @@ -218,3 +221,50 @@ transaction_print(FILE *f, } return 0; } + +int +transaction_log(clicon_handle h, + transaction_data th, + int level, + const char *op) +{ + cxobj *xn; + int i; + transaction_data_t *td; + cbuf *cb = NULL; + + td = (transaction_data_t *)th; + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_CFG, errno, "cbuf_new"); + goto done; + } + for (i=0; itd_dlen; i++){ + xn = td->td_dvec[i]; + clicon_xml2cbuf(cb, xn, 0, 0); + } + if (i) + clicon_log(level, "%s %" PRIu64 " %s del: %s", + __FUNCTION__, td->td_id, op, cbuf_get(cb)); + cbuf_reset(cb); + for (i=0; itd_alen; i++){ + xn = td->td_avec[i]; + clicon_xml2cbuf(cb, xn, 0, 0); + } + if (i) + clicon_log(level, "%s %" PRIu64 " %s add: %s", __FUNCTION__, td->td_id, op, cbuf_get(cb)); + cbuf_reset(cb); + for (i=0; itd_clen; i++){ + if (td->td_scvec){ + xn = td->td_scvec[i]; + clicon_xml2cbuf(cb, xn, 0, 0); + } + xn = td->td_tcvec[i]; + clicon_xml2cbuf(cb, xn, 0, 0); + } + if (i) + clicon_log(level, "%s %" PRIu64 " %s change: %s", __FUNCTION__, td->td_id, op, cbuf_get(cb)); + done: + if (cb) + cbuf_free(cb); + return 0; +} diff --git a/apps/backend/clixon_backend_transaction.h b/apps/backend/clixon_backend_transaction.h index 447cd15f..9698b35b 100644 --- a/apps/backend/clixon_backend_transaction.h +++ b/apps/backend/clixon_backend_transaction.h @@ -60,5 +60,6 @@ cxobj **transaction_tcvec(transaction_data td); size_t transaction_clen(transaction_data td); int transaction_print(FILE *f, transaction_data th); +int transaction_log(clicon_handle h, transaction_data th, int level, const char *id); #endif /* _CLIXON_BACKEND_TRANSACTION_H_ */ diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index 5c1fa800..a881134b 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -656,6 +656,12 @@ cli_show_auto(clicon_handle h, // goto done; if (api_path_fmt2xpath(api_path_fmt, cvv, &xpath) < 0) goto done; + /* XXX Kludge to overcome a trailing / in show, that I cannot add to + * yang2api_path_fmt_1 where it should belong. + */ + if (xpath[strlen(xpath)-1] == '/') + xpath[strlen(xpath)-1] = '\0'; + /* Get configuration from database */ if (clicon_rpc_get_config(h, db, xpath, &xt) < 0) goto done; diff --git a/example/main/example_backend.c b/example/main/example_backend.c index e4df5dd8..fe891335 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -72,23 +72,47 @@ static int _state = 0; */ static int _upgrade = 0; +/*! Variable to control transaction logging (for debug) + * If set, call syslog for every transaction callback + */ +static int _transaction_log = 0; + /* forward */ static int example_stream_timer_setup(clicon_handle h); +int +main_begin(clicon_handle h, + transaction_data td) +{ + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "begin"); + + return 0; +} /*! This is called on validate (and commit). Check validity of candidate */ int -transaction_validate(clicon_handle h, - transaction_data td) +main_validate(clicon_handle h, + transaction_data td) { - // transaction_print(stderr, td); + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "validate"); + return 0; +} + +int +main_complete(clicon_handle h, + transaction_data td) +{ + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "complete"); return 0; } /*! This is called on commit. Identify modifications and adjust machine state */ int -transaction_commit(clicon_handle h, +main_commit(clicon_handle h, transaction_data td) { cxobj *target = transaction_target(td); /* wanted XML tree */ @@ -96,7 +120,8 @@ transaction_commit(clicon_handle h, int i; size_t len; - clicon_debug(1, "%s", __FUNCTION__); + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "commit"); /* Get all added i/fs */ if (xpath_vec_flag(target, "//interface", XML_FLAG_ADD, &vec, &len) < 0) return -1; @@ -109,6 +134,24 @@ transaction_commit(clicon_handle h, return 0; } +int +main_end(clicon_handle h, + transaction_data td) +{ + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "end"); + return 0; +} + +int +main_abort(clicon_handle h, + transaction_data td) +{ + if (_transaction_log) + transaction_log(h, td, LOG_NOTICE, "abort"); + return 0; +} + /*! Routing example notification timer handler. Here is where the periodic action is */ static int @@ -521,12 +564,12 @@ static clixon_plugin_api api = { example_exit, /* exit */ .ca_reset=example_reset, /* reset */ .ca_statedata=example_statedata, /* statedata */ - .ca_trans_begin=NULL, /* trans begin */ - .ca_trans_validate=transaction_validate,/* trans validate */ - .ca_trans_complete=NULL, /* trans complete */ - .ca_trans_commit=transaction_commit, /* trans commit */ - .ca_trans_end=NULL, /* trans end */ - .ca_trans_abort=NULL /* trans abort */ + .ca_trans_begin=main_begin, /* trans begin */ + .ca_trans_validate=main_validate, /* trans validate */ + .ca_trans_complete=main_complete, /* trans complete */ + .ca_trans_commit=main_commit, /* trans commit */ + .ca_trans_end=main_end, /* trans end */ + .ca_trans_abort=main_abort /* trans abort */ }; /*! Backend plugin initialization @@ -551,7 +594,7 @@ clixon_plugin_init(clicon_handle h) goto done; opterr = 0; optind = 1; - while ((c = getopt(argc, argv, "rsu")) != -1) + while ((c = getopt(argc, argv, "rsut")) != -1) switch (c) { case 'r': _reset = 1; @@ -562,6 +605,9 @@ clixon_plugin_init(clicon_handle h) case 'u': _upgrade = 1; break; + case 't': /* transaction log */ + _transaction_log = 1; + break; } /* Example stream initialization: diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 7d6bc9f6..792099f7 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -49,6 +49,7 @@ int xml2cli(FILE *f, cxobj *x, char *prepend, enum genmodel_type gt); int xml_yang_root(cxobj *x, cxobj **xr); int xmlns_assign(cxobj *x); int xml_yang_validate_rpc(cxobj *xrpc, cbuf *cbret); +int xml_yang_validate_list_key_only(cxobj *xt, cbuf *cbret); int xml_yang_validate_add(cxobj *xt, cbuf *cbret); int xml_yang_validate_all(cxobj *xt, cbuf *cbret); int xml_yang_validate_all_top(cxobj *xt, cbuf *cbret); @@ -72,6 +73,8 @@ int xml_spec_populate(cxobj *x, void *arg); int api_path2xpath(yang_stmt *yspec, cvec *cvv, int offset, cbuf *xpath); int api_path2xml(char *api_path, yang_stmt *yspec, cxobj *xtop, yang_class nodeclass, int strict, cxobj **xpathp, yang_stmt **ypathp); + +int xml2xpath(cxobj *x, char **xpath); int xml_merge(cxobj *x0, cxobj *x1, yang_stmt *yspec, char **reason); int yang_enum_int_value(cxobj *node, int32_t *val); diff --git a/lib/src/clixon_datastore.c b/lib/src/clixon_datastore.c index 278a0023..d5b918c7 100644 --- a/lib/src/clixon_datastore.c +++ b/lib/src/clixon_datastore.c @@ -35,6 +35,10 @@ * Saves Clixon data as clear-text XML (or JSON) */ +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + #include #include #include @@ -193,7 +197,7 @@ xmldb_copy(clicon_handle h, db_elmnt de0 = {0,}; cxobj *x1 = NULL; /* from */ cxobj *x2 = NULL; /* to */ - + /* XXX lock */ if (clicon_option_bool(h, "CLICON_XMLDB_CACHE")){ /* Copy in-memory cache */ diff --git a/lib/src/clixon_datastore_tree.c b/lib/src/clixon_datastore_tree.c index 91c34fdb..6fc61dd8 100644 --- a/lib/src/clixon_datastore_tree.c +++ b/lib/src/clixon_datastore_tree.c @@ -82,6 +82,16 @@ struct indexhead{ }; typedef struct indexhead indexhead; +static int +indexhead_free(indexhead *ih) +{ + indexlist *il; + + while ((il = ih->ih_list) != NULL) + DELQ(il, ih->ih_list, indexlist*); + return 0; +} + int index_get(indexhead *ih, size_t len, /* in bytes */ @@ -178,8 +188,8 @@ index_dump(FILE *f, * @retval 1 OK xp set (or NULL if empty) */ static int -buf2xml(FILE *f, - uint32_t index, +buf2xml(FILE *f, + uint32_t index, cxobj **xp) { int retval = -1; @@ -190,7 +200,7 @@ buf2xml(FILE *f, cxobj *x = NULL; char *name; char *prefix = NULL; - char hdr[8]; + char hdr[8] = {0, }; char *buf = NULL; int vlen; int i; @@ -362,7 +372,8 @@ xml2buf(FILE *f, else{ uint32_t index; for (i=0; i< xml_child_nr(x); i++){ - if (xml2buf(f, xml_child_i(x, i), ih, &index) < 0) + if (xml2buf(f, xml_child_i(x, i), ih, + &index) < 0) goto done; index = htonl(index); memcpy(&buf[ptr], &index, sizeof(uint32_t)); @@ -376,10 +387,6 @@ xml2buf(FILE *f, goto done; } memcpy(buf0, hdr, sizeof(hdr)); - if (0 && fwrite(hdr, sizeof(char), sizeof(hdr), f) < 0){ - clicon_err(OE_UNIX, errno, "fwrite"); - goto done; - } if (fwrite(buf0, sizeof(char), len, f) < 0){ clicon_err(OE_XML, errno, "fwrite"); goto done; @@ -401,7 +408,6 @@ datastore_tree_write(clicon_handle h, int retval = -1; FILE *f = NULL; indexhead ih = {0,}; - indexlist *il; if ((f = fopen(filename, "w+b")) == NULL){ clicon_err(OE_XML, errno, "Opening file %s", filename); @@ -413,8 +419,7 @@ datastore_tree_write(clicon_handle h, index_dump(stderr, &ih); retval = 0; done: - while ((il = ih.ih_list) != NULL) - DELQ(il, ih.ih_list, indexlist*); + indexhead_free(&ih); if (f != NULL) fclose(f); return retval; @@ -439,7 +444,8 @@ datastore_tree_read(clicon_handle h, } if ((retval = buf2xml(f, 0, xt)) < 0) goto done; - if (retval == 0){ /* fail */ + if (retval == 0 || /* fail */ + *xt == NULL){ /* empty */ if ((*xt = xml_new("config", NULL, NULL)) == NULL) goto done; xml_type_set(*xt, CX_ELMNT); diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index ac43a616..e799b166 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -633,6 +633,44 @@ check_mandatory(cxobj *xt, goto done; } +static int +check_list_key(cxobj *xt, + yang_stmt *yt, + cbuf *cbret) +{ + int retval = -1; + int i; + yang_stmt *yc; + cvec *cvk = NULL; /* vector of index keys */ + cg_var *cvi; + char *keyname; + + for (i=0; iys_len; i++){ + yc = yt->ys_stmt[i]; + /* Check if a list does not have mandatory key leafs */ + if (yt->ys_keyword == Y_LIST && + yc->ys_keyword == Y_KEY && + yang_config(yt)){ + cvk = yt->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ + cvi = NULL; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + if (xml_find_type(xt, NULL, keyname, CX_ELMNT) == NULL){ + if (netconf_missing_element(cbret, "application", keyname, "Mandatory key") < 0) + goto done; + goto fail; + } + } + } + } + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + /*! Validate a single XML node with yang specification for added entry * 1. Check if mandatory leafs present as subs. * 2. Check leaf values, eg int ranges and string regexps. @@ -737,6 +775,41 @@ xml_yang_validate_add(cxobj *xt, goto done; } +/*! Some checks done only at edit_config, eg keys in lists + */ +int +xml_yang_validate_list_key_only(cxobj *xt, + cbuf *cbret) +{ + int retval = -1; + yang_stmt *yt; /* yang spec of xt going in */ + int ret; + cxobj *x; + + /* if not given by argument (overide) use default link + and !Node has a config sub-statement and it is false */ + if ((yt = xml_spec(xt)) != NULL && yang_config(yt) != 0){ + if ((ret = check_list_key(xt, yt, cbret)) < 0) + goto done; + if (ret == 0) + goto fail; + } + x = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + if ((ret = xml_yang_validate_list_key_only(x, cbret)) < 0) + goto done; + if (ret == 0) + goto fail; + } + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + + /*! Validate a single XML node with yang specification for all (not only added) entries * 1. Check leafrefs. Eg you delete a leaf and a leafref references it. * @param[in] xt XML node to be validated @@ -1126,11 +1199,13 @@ xml_diff1(yang_stmt *ys, if (cxvec_append(x0c, x0vec, x0veclen) < 0) goto done; x0c = xml_child_each(x0, x0c, CX_ELMNT); + continue; } else if (eq > 0){ if (cxvec_append(x1c, x1vec, x1veclen) < 0) goto done; x1c = xml_child_each(x1, x1c, CX_ELMNT); + continue; } else{ /* equal */ if ((yc = xml_spec(x0c)) == NULL){ @@ -1257,8 +1332,21 @@ yang2api_path_fmt_1(yang_stmt *ys, yp->ys_keyword != Y_SUBMODULE){ if (yang2api_path_fmt_1((yang_stmt *)yp, 1, cb) < 0) /* recursive call */ goto done; - if (yp->ys_keyword != Y_CHOICE && yp->ys_keyword != Y_CASE) - cprintf(cb, "/"); + if (yp->ys_keyword != Y_CHOICE && yp->ys_keyword != Y_CASE){ +#if 0 + /* In some cases, such as cli_show_auto, a trailing '/' should + * NOT be present if ys is a key in a list. + * But in other cases (I think most), the / should be there, + * so a patch is added in cli_show_auto instead. + */ + if (ys->ys_keyword == Y_LEAF && yp && + yp->ys_keyword == Y_LIST && + yang_key_match(yp, ys->ys_argument) == 1) + ; + else +#endif + cprintf(cb, "/"); + } } else /* top symbol - mark with name prefix */ cprintf(cb, "/%s:", yp->ys_argument); @@ -2226,6 +2314,112 @@ api_path2xml(char *api_path, goto done; } +/*! Given an XML node, build an xpath to root, internal function + * @retval 0 OK + * @retval -1 Error. eg XML malformed + */ +static int +xml2xpath1(cxobj *x, + cbuf *cb) +{ + int retval = -1; + cxobj *xp; + yang_stmt *y = NULL; + cvec *cvk = NULL; /* vector of index keys */ + cg_var *cvi; + char *keyname; + cxobj *xkey; + cxobj *xb; + char *b; + enum rfc_6020 keyword; + + if ((xp = xml_parent(x)) != NULL && + xml_spec(xp) != NULL) + xml2xpath1(xp, cb); + /* XXX: sometimes there should be a /, sometimes not */ + cprintf(cb, "/%s", xml_name(x)); + if ((y = xml_spec(x)) != NULL){ + keyword = yang_keyword_get(y); + if (keyword == Y_LEAF_LIST){ + if ((b = xml_body(x)) != NULL) + cprintf(cb, "[.=\"%s\"]", b); + else + cprintf(cb, "[.=\"\"]"); + } else if (keyword == Y_LIST){ + cvk = yang_cvec_get(y); + cvi = NULL; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + if ((xkey = xml_find(x, keyname)) == NULL) + goto done; /* No key in xml */ + if ((xb = xml_find(x, keyname)) == NULL) + goto done; + b = xml_body(xb); + cprintf(cb, "[%s=\"%s\"]", keyname, b?b:""); + } + } + } + retval = 0; + done: + return retval; +} + +/*! Given an XML node, build an xpath to root + * Builds only unqualified xpaths, ie no predicates [] + * @param[in] x XML object + * @param[out] xpath Malloced xpath string. Need to free() after use + * @retval 0 OK + * @retval -1 Error. (eg XML malformed) + */ +int +xml2xpath(cxobj *x, + char **xpathp) +{ + int retval = -1; + cbuf *cb; + char *xpath = NULL; + + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (xml2xpath1(x, cb) < 0) + goto done; + /* XXX: see xpath in test statement,.. */ + xpath = cbuf_get(cb); +#if 0 /* debug test */ + { + cxobj *xt = x; + cxobj *xcp; + cxobj *x2; + while (xml_parent(xt) != NULL && + xml_spec(xt) != NULL) + xt = xml_parent(xt); + xcp = xml_parent(xt); + xml_parent_set(xt, NULL); + x2 = xpath_first(xt, "%s", xpath); /* +1: skip first / */ + xml_parent_set(xt, xcp); + assert(x2 && x==x2); + if (x==x2) + clicon_debug(1, "%s %s match", __FUNCTION__, xpath); + else + clicon_debug(1, "%s %s no match", __FUNCTION__, xpath); + } +#endif + if (xpathp){ + if ((*xpathp = strdup(xpath)) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + xpath = NULL; + } + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + /*! Check if the module tree x is in is assigned right XML namespace, assign if not * @param[in] x XML node *(0. You should probably find the XML root and apply this function to that.) diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index a2854d3d..034cb4a7 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -715,18 +715,16 @@ xp_relop(xp_ctx *xc1, s1 = xml_body(x); switch(op){ case XO_EQ: - if (s1==NULL || s2==NULL){ - clicon_err(OE_XML, EINVAL, "Malformed xpath: empty string"); - goto done; - } - xr->xc_bool = (strcmp(s1, s2)==0); + if (s1 == NULL || s2 == NULL) + xr->xc_bool = (s1==NULL && s2 == NULL); + else + xr->xc_bool = (strcmp(s1, s2)==0); break; case XO_NE: - if (s1==NULL || s2==NULL){ - clicon_err(OE_XML, EINVAL, "Malformed xpath: empty string"); - goto done; - } - xr->xc_bool = (strcmp(s1, s2)); + if (s1 == NULL || s2 == NULL) + xr->xc_bool = !(s1==NULL && s2 == NULL); + else + xr->xc_bool = (strcmp(s1, s2)); break; default: clicon_err(OE_XML, 0, "Operator %s not supported for nodeset and string", clicon_int2str(xpopmap,op)); diff --git a/test/long.sh b/test/long.sh index 55191fe7..37d44537 100755 --- a/test/long.sh +++ b/test/long.sh @@ -7,11 +7,14 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi # Number of list/leaf-list entries in file -: ${perfnr:=1000} +: ${perfnr:=5000} # Number of requests made get/put : ${perfreq:=100} +# Which format to use as datastore format internally +: ${format:=xml} + APPNAME=example cfg=$dir/scaling-conf.xml @@ -50,11 +53,14 @@ cat < $cfg /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/var/$APPNAME/$APPNAME.pidfile false + $format /usr/local/var/$APPNAME false EOF +sudo callgrind_control -i off + new "test params: -f $cfg -y $fyang" if [ $BE -ne 0 ]; then new "kill old backend" @@ -76,17 +82,39 @@ start_restconf -f $cfg -y $fyang new "waiting" sleep $RCWAIT +new "generate 'large' config with $perfnr list entries" +echo -n "" > $fconfig +for (( i=0; i<$perfnr; i++ )); do + echo -n "$i$i" >> $fconfig +done +echo "]]>]]>" >> $fconfig + +# Now take large config file and write it via netconf to candidate +new "netconf write large config" +expecteof_file "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" + +# Now commit it from candidate to running +new "netconf commit large config" +expecteof "/usr/bin/time -f %e $clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" "^]]>]]>$" + +# Zero all event counters +sudo callgrind_control -i on +sudo callgrind_control -z + while [ 1 ] ; do new "restconf add $perfreq small config" - time -p for (( i=0; i<$perfreq; i++ )); do + +time -p for (( i=0; i<$perfreq; i++ )); do +#echo "i $i" rnd=$(( ( RANDOM % $perfnr ) )) curl -s -X PUT http://localhost/restconf/data/scaling:x/y=$rnd -d '{"scaling:y":{"a":"'$rnd'","b":"'$rnd'"}}' done +done new "restconf get $perfreq small config" time -p for (( i=0; i<$perfreq; i++ )); do rnd=$(( ( RANDOM % $perfnr ) )) - curl -sG http://localhost/restconf/data/scaling:x/y=$rnd,$rnd > /dev/null + curl -sG http://localhost/restconf/data/scaling:x/y=$rnd,42 > /dev/null done done diff --git a/test/mem.sh b/test/mem.sh index 94ed4b51..69d4981b 100755 --- a/test/mem.sh +++ b/test/mem.sh @@ -3,7 +3,7 @@ # Stop on first error # Run valgrindtest once, args: -# what: cli|netconf|restconf|backend +# what: (cli|netconf|restconf|backend)* # no args means all memonce(){ what=$1 diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index cc457c60..15b16fcd 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -10,6 +10,9 @@ APPNAME=example cfg=$dir/conf_yang.xml fyang=$dir/nacm-example.yang +# Which format to use as datastore format internally +: ${format:=xml} + cat < $cfg $cfg @@ -29,6 +32,7 @@ cat < $cfg /usr/local/lib/xmldb/text.so false internal + $format EOF diff --git a/test/test_netconf.sh b/test/test_netconf.sh index 3b369875..129cb93a 100755 --- a/test/test_netconf.sh +++ b/test/test_netconf.sh @@ -33,7 +33,7 @@ cat < $cfg EOF -new "test params: -f $cfg" +new "test params: -f $cfg -- -s" # Bring your own backend if [ $BE -ne 0 ]; then # kill old backend (if any) @@ -88,10 +88,7 @@ new "Re-Delete eth/0/0 using none should generate error" expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^' new "Add interface without key" -expecteof "$clixon_netconf -qf $cfg" 0 'ex:ethnone ]]>]]>' "^]]>]]>$" - -new "netconf validate missing key" -expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^applicationmissing-elementnameerrorMandatory key]]>]]>$' +expecteof "$clixon_netconf -qf $cfg" 0 'ex:ethnone ]]>]]>' '^applicationmissing-elementnameerrorMandatory key]]>]]>$' new "netconf discard-changes" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" diff --git a/test/test_order.sh b/test/test_order.sh index 82171613..0408d03e 100755 --- a/test/test_order.sh +++ b/test/test_order.sh @@ -9,6 +9,9 @@ # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi +# Which format to use as datastore format internally +: ${format:=xml} + APPNAME=example cfg=$dir/conf_yang.xml @@ -37,6 +40,7 @@ cat < $cfg /usr/local/lib/example/backend /usr/local/var/$APPNAME/$APPNAME.pidfile $dbdir + $format EOF diff --git a/test/test_perf.sh b/test/test_perf.sh index 75bc0ed5..7adb1ba4 100755 --- a/test/test_perf.sh +++ b/test/test_perf.sh @@ -4,6 +4,9 @@ # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi +# Which format to use as datastore format internally +: ${format:=xml} + # Number of list/leaf-list entries in file : ${perfnr:=10000} @@ -17,7 +20,6 @@ fyang=$dir/scaling.yang fconfig=$dir/large.xml fconfig2=$dir/large2.xml -format=xml cat < $fyang module scaling{ @@ -59,6 +61,7 @@ cat < $cfg 1 VARS 0 + ietf-netconf:startup EOF @@ -117,10 +120,11 @@ for mode in startup running; do clixon_util_datastore -d ${mode} -f tree -y $fyang -b $dir -x $tmpx put create ;; esac - new "Startup backend $format -s $mode -f $cfg -y $fyang" - echo "time sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang -o CLICON_XMLDB_FORMAT=$format" + new "Startup format: $format mode:$mode" +# echo "time sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang -o CLICON_XMLDB_FORMAT=$format" # Cannot use start_backend here due to expected error case - time sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang -o CLICON_XMLDB_FORMAT=$format # 2> /dev/null +{ time -p sudo $clixon_backend -F1 -D $DBG -s $mode -f $cfg -y $fyang -o CLICON_XMLDB_FORMAT=$format 2> /dev/null; } 2>&1 | awk '/real/ {print $2}' + done done @@ -189,7 +193,9 @@ time -p for (( i=0; i<$perfreq; i++ )); do curl -sG http://localhost/restconf/data/scaling:x/y=$rnd > /dev/null done -# Reference: i686 perfnr=10000 time: 27s 20190425 34s WITH startup copying +# Reference: +# i686 format=xml perfnr=10000/100 time: 38/29s 20190425 WITH/OUT startup copying +# i686 format=tree perfnr=10000/100 time: 72/64s 20190425 WITH/OUT startup copying new "restconf add $perfreq small config" time -p for (( i=0; i<$perfreq; i++ )); do rnd=$(( ( RANDOM % $perfnr ) )) diff --git a/test/test_startup.sh b/test/test_startup.sh index cd36277c..691c7df7 100755 --- a/test/test_startup.sh +++ b/test/test_startup.sh @@ -13,6 +13,9 @@ # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi +# Which format to use as datastore format internally +: ${format:=xml} + APPNAME=example cfg=$dir/conf_startup.xml @@ -36,6 +39,7 @@ cat < $cfg 0 init false + $format EOF diff --git a/test/test_transaction.sh b/test/test_transaction.sh new file mode 100755 index 00000000..7eb1f7ad --- /dev/null +++ b/test/test_transaction.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Transaction functionality +# The test uses a backend that logs to a file and a netconf client to push +# changes. The main example backend plugin logs to the file, and the test +# verifies the logs. +# The yang is a list with three members, so that you can do add/delete/change +# in a single go. + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +# Which format to use as datastore format internally +: ${format:=xml} + +APPNAME=example + +cfg=$dir/conf_yang.xml +fyang=$dir/trans.yang +flog=$dir/backend.log +touch $flog + +cat < $fyang +module trans{ + yang-version 1.1; + namespace "urn:example:clixon"; + prefix ex; + container x { + list y { + key "a"; + leaf a { + type int32; + } + leaf b { + description "change this"; + type int32; + } + leaf c { + description "del this"; + type int32; + } + leaf d { + description "add this"; + type int32; + } + } + } +} +EOF + +cat < $cfg + + $cfg + /usr/local/share/clixon + $fyang + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/backend + example_backend.so$ + /usr/local/lib/$APPNAME/netconf + /usr/local/lib/$APPNAME/restconf + /usr/local/lib/$APPNAME/cli + $APPNAME + $dir/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + 1 + /usr/local/var/$APPNAME + $format + +EOF + +# Check statements in log +checklog(){ + s=$1 # statement + new "Check $s in log" + t=$(grep "$s" $flog) + if [ -z "$t" ]; then + echo -e "\e[31m\nError in Test$testnr [$testname]:" + if [ $# -gt 0 ]; then + echo "Not found in log" + echo + fi + echo -e "\e[0m" + exit -1 + fi +} + +new "test params: -f $cfg -l f$flog -- -t" +# Bring your own backend +if [ $BE -ne 0 ]; then + # kill old backend (if any) + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg -l f$flog -- -t" + start_backend -s init -f $cfg -l f$flog -- -t # -t means transaction logging + new "waiting" + sleep $RCWAIT +fi + +new "netconf base config (0) a,b,c" +expecteof "$clixon_netconf -qf $cfg" 0 '000]]>]]>' '^]]>]]>$' + +new "netconf commit base" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' +#Ignore + +new "netconf mixed change: change b, del c, add d" +expecteof "$clixon_netconf -qf $cfg" 0 '0420]]>]]>' '^]]>]]>$' + +new "netconf commit change" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' + +# Check complete transaction 2: +for op in begin validate complete commit; do + checklog "transaction_log 2 $op add: 0" + checklog "transaction_log 2 $op change: 042" +done +# End is special +checklog "transaction_log 2 end add: 0" +checklog "transaction_log 2 end change: 42" + +new "netconf config (1) end-points a,d " +expecteof "$clixon_netconf -qf $cfg" 0 '11]]>]]>' '^]]>]]>$' + +new "netconf commit base" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' + +new "netconf insert b,c between end-points" +expecteof "$clixon_netconf -qf $cfg" 0 '111]]>]]>' '^]]>]]>$' + +new "netconf commit base" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' + +checklog "transaction_log 4 validate add: 11" + +if [ $BE -eq 0 ]; then + exit # BE +fi + +new "Kill backend" +# Check if premature kill +pid=`pgrep -u root -f clixon_backend` +if [ -z "$pid" ]; then + err "backend already dead" +fi +# kill backend +stop_backend -f $cfg + +#rm -rf $dir diff --git a/test/test_type.sh b/test/test_type.sh index 1b01d7e8..26d09a53 100755 --- a/test/test_type.sh +++ b/test/test_type.sh @@ -9,12 +9,14 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi APPNAME=example +# Which format to use as datastore format internally +: ${format:=xml} + cfg=$dir/conf_yang.xml fyang=$dir/type.yang fyang2=$dir/example2.yang fyang3=$dir/example3.yang - # transitive type, exists in fyang3, referenced from fyang2, but not declared in fyang cat < $fyang3 module example3{ @@ -214,6 +216,7 @@ testrun(){ 1 /usr/local/var/$APPNAME $dbcache + $format EOF @@ -236,6 +239,10 @@ EOF new "cli set transitive string error. Wrong type" expectfn "$clixon_cli -1f $cfg -l o -y $fyang set c talle 9xx" 255 '^CLI syntax error: "set c talle 9xx": Unknown command$' + new "netconf discard-changes" + expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 "]]>]]>" "^]]>]]>$" + + new "netconf set transitive string error" expecteof "$clixon_netconf -qf $cfg -y $fyang" 0 '9xx]]>]]>' "^]]>]]>" @@ -605,10 +612,10 @@ EOF fi } -# Run with db cache -testrun true - # Run without db cache testrun false +# Run with db cache +testrun true + rm -rf $dir diff --git a/util/clixon_util_insert.c b/util/clixon_util_insert.c index 1edf35f7..0278ada8 100644 --- a/util/clixon_util_insert.c +++ b/util/clixon_util_insert.c @@ -93,7 +93,7 @@ main(int argc, char **argv) char *x0str = NULL; char *xistr = NULL; char *xpath = NULL; - yang_stmt *yspec; + yang_stmt *yspec = NULL; cxobj *x0 = NULL; cxobj *xb; cxobj *xi = NULL; diff --git a/yang/clixon/clixon-config@2019-03-05.yang b/yang/clixon/clixon-config@2019-03-05.yang index d8a1a366..39f535bf 100644 --- a/yang/clixon/clixon-config@2019-03-05.yang +++ b/yang/clixon/clixon-config@2019-03-05.yang @@ -92,7 +92,7 @@ module clixon-config { } enum tree{ description "Save and load xmldb as Clixon record-based tree - file format"; + file format (experimental)"; } } } @@ -372,7 +372,7 @@ module clixon-config { type boolean; default true; description - "XMLDB datastore cache. + "XMLDB datatsore cache. If set, XML candidate/running parsed tree is stored in memory If not set, candidate/running is always accessed via disk."; }