diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bd5dd1e..8b5a8f36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,34 @@ Expected: July 2020 ### API changes on existing protocol/config features (For users) +* Top-level default leafs assigned. + * Enforcing RFC 7950 Sec 7.6.1 means unassigned top-level leafs (or leafs under non-presence containers) are assigned default values. + * In this process non-presence containers may be created. + * See also [default values don't show up in datastores #111](https://github.com/clicon/clixon/issues/111). +* NACM default behaviour is read-only (empty configs are dead-lockedd) + * This applies if NACM is loaded and `CLICON_NACM_MODE` is `internal` + * Due to the previous bult (top-level default leafs) + * This means that empty configs or empty NACM configs are not writable (deadlocked). + * Workarounds: + 1. Access the system with the recovery user, see clixon option CLICON_NACM_RECOVERY_USER + + * This means that empty configs or empty NACM configs are not writable (deadlocked). + * Workarounds: + 1. Access the system with the recovery user, see clixon option CLICON_NACM_RECOVERY_USER + + * This means that empty configs or empty NACM configs are not writable (deadlocked). + * Workarounds: + 1. Access the system with the recovery user, see clixon option CLICON_NACM_RECOVERY_USER + + * This means that empty configs or empty NACM configs are not writable (deadlocked). + * Workarounds: + 1. Access the system with the recovery user, see clixon option CLICON_NACM_RECOVERY_USER + + * This means that empty configs or empty NACM configs are not writable (deadlocked). + * Workarounds: + 1. Access the system with the recovery user, see clixon option CLICON_NACM_RECOVERY_USER + 2. Edit the startup-db with a valid NACM config and restart the system + 3. Set clixon option CLICON_NACM_DISABLED_ON_EMPTY to true which means that if the config is (completely) empty, you can add a NACM config as a first edit. * Netconf lock/unlock behaviour changed to adhere to RFC 6241 * Changed commit lock error tag from "lock denied" to "in-use". * Changed unlock error message from "lock is already held" to #lock not active" or "lock held by other session". @@ -53,6 +81,7 @@ Expected: July 2020 * CLICON_SSL_SERVER_CERT * CLICON_SSL_SERVER_KEY * CLICON_SSL_CA_CERT + * Added CLICON_NACM_DISABLED_ON_EMPTY to mitigate read-only "dead-lock" of empty startup configs. * Restconf FCGI (eg via nginx) have changed reply message syntax slightly as follows (due to refactoring and common code with evhtp): * Bodies in error retuns including html code have been removed * Some (extra) CRLF:s have been removed @@ -99,6 +128,8 @@ Expected: July 2020 ### Corrected Bugs +* Fixed: [default values don't show up in datastores #111](https://github.com/clicon/clixon/issues/111). + * See also API changes since this changes NACM behavior for example. * Fixed: Don't call upgrade callbacks if no revision defined so there's no way to determine right way 'from' and 'to' * Fixed: [lock candidate succeeded even though it is modified #110](https://github.com/clicon/clixon/issues/110) * Fixed: [Need to add the possibility to use anchors around patterns #51](https://github.com/clicon/cligen/issues/51): diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index 52be4f30..21f2a579 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -226,7 +226,7 @@ startup_extraxml(clicon_handle h, /* Clear tmp db */ if (xmldb_db_reset(h, tmp_db) < 0) goto done; - /* Application may define extra xml in its reset function*/ + /* Application may define extra xml in its reset function */ if (clixon_plugin_reset_all(h, tmp_db) < 0) goto done; /* Extra XML can also be added via file */ @@ -238,13 +238,13 @@ startup_extraxml(clicon_handle h, goto fail; } /* - * Check if tmp db is empty. + * Check if tmp db is empty. XXX no this is not possible. * It should be empty if extra-xml is null and reset plugins did nothing * then skip validation. */ if (xmldb_get(h, tmp_db, NULL, NULL, &xt0) < 0) goto done; - if (xt0==NULL || xml_child_nr(xt0)==0) + if (xmldb_empty_get(h, tmp_db)) goto ok; xt = NULL; /* Validate the tmp db and return possibly upgraded xml in xt diff --git a/lib/clixon/clixon_data.h b/lib/clixon/clixon_data.h index bf46003d..79641950 100644 --- a/lib/clixon/clixon_data.h +++ b/lib/clixon/clixon_data.h @@ -54,7 +54,8 @@ typedef struct { uint32_t de_id; /* session id */ cxobj *de_xml; /* cache */ - int de_modified; + int de_modified; /* Dirty since loaded/copied/committed/etc XXX:nocache? */ + int de_empty; /* Empty on read from file, xmldb_readfile and xmldb_put sets it */ } db_elmnt; /* diff --git a/lib/clixon/clixon_datastore.h b/lib/clixon/clixon_datastore.h index c0bacc7f..30bbf4e3 100644 --- a/lib/clixon/clixon_datastore.h +++ b/lib/clixon/clixon_datastore.h @@ -73,7 +73,7 @@ cxobj *xmldb_cache_get(clicon_handle h, const char *db); int xmldb_modified_get(clicon_handle h, const char *db); int xmldb_modified_set(clicon_handle h, const char *db, int value); - +int xmldb_empty_get(clicon_handle h, const char *db); int xmldb_dump(clicon_handle h, FILE *f, cxobj *xt); #endif /* _CLIXON_DATASTORE_H */ diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index ad404a7a..15adc657 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -66,6 +66,9 @@ int xml_tree_prune_flagged(cxobj *xt, int flag, int test); int xml_namespace_change(cxobj *x, char *ns, char *prefix); int xml_default(cxobj *x); int xml_default_recurse(cxobj *xn); +int xml_default_yspec(yang_stmt *yspec, cxobj *xn); +int xml_nopresence_default(cxobj *xt); +int xml_nopresence_default_mark(cxobj *x, void *arg); int xml_sanity(cxobj *x, void *arg); int xml_non_config_data(cxobj *xt, void *arg); diff --git a/lib/clixon/clixon_yang_module.h b/lib/clixon/clixon_yang_module.h index 583500e4..11a91971 100644 --- a/lib/clixon/clixon_yang_module.h +++ b/lib/clixon/clixon_yang_module.h @@ -49,12 +49,13 @@ /* Struct containing module state differences between two modules or two * revisions of same module. - * This is in state of flux so it needs to be contained and easily changed. + * The most significant usecase is one module-state is a loaded datastore and the other + * is the one loaded by the server by its YANG files. */ typedef struct { int md_status; /* 0 if no module-state in a datastore, 1 if there is */ char *md_set_id; /* server-specific identifier */ - cxobj *md_diff; /* yang module state containing revisions and XML_FLAG_ADD|DEL|CHANGE */ + cxobj *md_diff; /* yang module state containing revisions and XML_FLAG_ADD|DEL|CHANGE */ } modstate_diff_t; /* diff --git a/lib/src/clixon_data.c b/lib/src/clixon_data.c index cada69b6..e46b2b6e 100644 --- a/lib/src/clixon_data.c +++ b/lib/src/clixon_data.c @@ -636,12 +636,11 @@ clicon_argv_set(clicon_handle h, return retval; } -/*! Get xml database element including pid and xml cache +/*! Get xml database element including id, xml cache, empty on startup and dirty bit * @param[in] h Clicon handle * @param[in] db Name of database * @retval de Database element * @retval NULL None found - * @note these use db_elmnt hash, not data */ db_elmnt * clicon_db_elmnt_get(clicon_handle h, @@ -655,14 +654,12 @@ clicon_db_elmnt_get(clicon_handle h, return NULL; } -/*! Set xml database element including pid and xml cache +/*! Set xml database element including id, xml cache, empty on startup and dirty bit * @param[in] h Clicon handle * @param[in] db Name of database * @param[in] de Database element * @retval 0 OK * @retval -1 Error - * XXX add prefix to db to ensure uniqueness? - * @note these use db_elmnt hash, not data */ int clicon_db_elmnt_set(clicon_handle h, diff --git a/lib/src/clixon_datastore.c b/lib/src/clixon_datastore.c index b091c08d..be771457 100644 --- a/lib/src/clixon_datastore.c +++ b/lib/src/clixon_datastore.c @@ -518,6 +518,26 @@ xmldb_modified_get(clicon_handle h, return de->de_modified; } +/*! Get empty flag from datastore (the datastore was empty ON LOAD) + * @param[in] h Clicon handle + * @param[in] db Database name + * @retval -1 Error (datastore does not exist) + * @retval 0 Db was not empty on load + * @retval 1 Db was empty on load + */ +int +xmldb_empty_get(clicon_handle h, + const char *db) +{ + db_elmnt *de; + + if ((de = clicon_db_elmnt_get(h, db)) == NULL){ + clicon_err(OE_CFG, EFAULT, "datastore %s does not exist", db); + return -1; + } + return de->de_empty; +} + /*! Get modified flag from datastore * @param[in] h Clicon handle * @param[in] db Database name diff --git a/lib/src/clixon_datastore_read.c b/lib/src/clixon_datastore_read.c index ac0acd9f..bcb686d2 100644 --- a/lib/src/clixon_datastore_read.c +++ b/lib/src/clixon_datastore_read.c @@ -72,6 +72,7 @@ #include "clixon_xpath.h" #include "clixon_json.h" #include "clixon_nacm.h" +#include "clixon_path.h" #include "clixon_netconf_lib.h" #include "clixon_yang_module.h" #include "clixon_xml_map.h" @@ -104,7 +105,7 @@ singleconfigroot(cxobj *xt, } } if (i != 1){ - clicon_err(OE_DB, ENOENT, "Top-element is not unique, expecting single config"); + clicon_err(OE_DB, ENOENT, "Top-element is not unique, expecting single config"); goto done; } x = NULL; @@ -443,33 +444,63 @@ text_read_modstate(clicon_handle h, return retval; } +static int +disable_nacm_on_empty(cxobj *x0, + yang_stmt *yspec) +{ + int retval = -1; + yang_stmt *ymod; + cxobj **vec = NULL; + int len = 0; + cxobj *xb; + + if ((ymod = yang_find(yspec, Y_MODULE, "ietf-netconf-acm")) == NULL) + goto ok; + if (clixon_xml_find_instance_id(x0, yspec, &vec, &len, "/nacm:nacm/nacm:enable-nacm") < 1) + goto done; + if (len){ + if ((xb = xml_body_get(vec[0])) == NULL) + goto done; + if (xml_value_set(xb, "false") < 0) + goto done; + } + if (vec) + free(vec); + ok: + retval = 0; + done: + return retval; +} + /*! Common read function that reads an XML tree from file * @param[in] th Datastore text handle * @param[in] db Symbolic database name, eg "candidate", "running" * @param[in] yb How to bind yang to XML top-level when parsing * @param[in] yspec Top-level yang spec * @param[out] xp XML tree read from file + * @param[out] de If set, return db-element status (eg empty flag) * @param[out] msdiff If set, return modules-state differences * @retval -1 Error * @retval 0 Parse OK but yang assigment not made (or only partial) and xerr set * @retval 1 OK - * @note retval 0 is NYI becaues of functions calling this function + * @note retval 0 is NYI because of functions calling this function cannot handle it yet */ -#undef XMLDB_READFILE_FAIL +#undef XMLDB_READFILE_FAIL /* See comment on retval = 0 above */ int xmldb_readfile(clicon_handle h, const char *db, yang_bind yb, yang_stmt *yspec, cxobj **xp, + db_elmnt *de, modstate_diff_t *msdiff) { - int retval = -1; - cxobj *x0 = NULL; - char *dbfile = NULL; - int fd = -1; - char *format; - int ret; + int retval = -1; + cxobj *x0 = NULL; + char *dbfile = NULL; + int fd = -1; + char *format; + int ret; if (xmldb_db2file(h, db, &dbfile) < 0) goto done; @@ -497,8 +528,9 @@ xmldb_readfile(clicon_handle h, goto fail; #endif /* Always assert a top-level called "config". - To ensure that, deal with two cases: - 1. File is empty -> rename top-level to "config" */ + * To ensure that, deal with two cases: + * 1. File is empty -> rename top-level to "config" + */ if (xml_child_nr(x0) == 0){ if (xml_name_set(x0, "config") < 0) goto done; @@ -509,7 +541,10 @@ xmldb_readfile(clicon_handle h, if (singleconfigroot(x0, &x0) < 0) goto done; } - /* From Clixon 3.10,datastore files may contain module-state defining + if (xml_child_nr(x0) == 0 && de) + de->de_empty = 1; + + /* Datastore files may contain module-state defining * which modules are used in the file. */ if (text_read_modstate(h, yspec, x0, msdiff) < 0) @@ -569,16 +604,36 @@ xmldb_get_nocache(clicon_handle h, size_t xlen; int i; int ret; + db_elmnt de0 = {0,}; + int empty = 0; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } - if ((ret = xmldb_readfile(h, db, YB_MODULE, yspec, &xt, msdiff)) < 0) + if ((ret = xmldb_readfile(h, db, YB_MODULE, yspec, &xt, &de0, msdiff)) < 0) goto done; if (ret == 0) goto fail; + clicon_db_elmnt_set(h, db, &de0); /* Content is copied */ + /* Check if empty */ + if (xml_child_nr(xt) == 0) + empty = 1; + /* Add global defaults. + * Must do it before xpath check, since globals may be filtered out + */ + if (xml_default_yspec(yspec, xt) < 0) + goto done; + /* If empty database, then disable NACM if loaded + * This has some drawbacks. One is that a config may become empty at a later stage + * and then this does not hold. + */ + if (empty && + clicon_option_bool(h, "CLICON_NACM_DISABLED_ON_EMPTY")){ + if (disable_nacm_on_empty(xt, yspec) < 0) + goto done; + } /* Here xt looks like: ... */ /* Given the xpath, return a vector of matches in xvec */ if (xpath_vec(xt, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) @@ -664,6 +719,7 @@ xmldb_get_cache(clicon_handle h, cxobj *x1t = NULL; db_elmnt de0 = {0,}; int ret; + int empty = 0; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); @@ -672,7 +728,7 @@ xmldb_get_cache(clicon_handle h, de = clicon_db_elmnt_get(h, db); if (de == NULL || de->de_xml == NULL){ /* Cache miss, read XML from file */ /* If there is no xml x0 tree (in cache), then read it from file */ - if ((ret = xmldb_readfile(h, db, yb, yspec, &x0t, msdiff)) < 0) + if ((ret = xmldb_readfile(h, db, yb, yspec, &x0t, &de0, msdiff)) < 0) goto done; if (ret == 0) goto fail; @@ -680,10 +736,30 @@ xmldb_get_cache(clicon_handle h, * No, argument against: we may want to have a semantically wrong file and wish to edit? */ de0.de_xml = x0t; - clicon_db_elmnt_set(h, db, &de0); + clicon_db_elmnt_set(h, db, &de0); /* Content is copied */ } /* x0t == NULL */ else x0t = de->de_xml; + + /* Check if empty, must be before add global defaults */ + if (xml_child_nr(x0t) == 0) + empty = 1; + + /* Add global defaults. + * Cant do it to x1t since that is after xpath check, since globals may be filtered out + */ + if (xml_default_yspec(yspec, x0t) < 0) + goto done; + /* If empty database, then disable NACM if loaded + * This has some drawbacks. One is that a config may become empty at a later stage + * and then this does not hold. + */ + if (empty && + clicon_option_bool(h, "CLICON_NACM_DISABLED_ON_EMPTY")){ + if (disable_nacm_on_empty(x0t, yspec) < 0) + goto done; + } + /* Here x0t looks like: ... */ /* Given the xpath, return a vector of matches in xvec * Can we do everything in one go? @@ -693,8 +769,6 @@ xmldb_get_cache(clicon_handle h, * a) for every node that is found, copy to new tree * b) if config dont dont state data */ - - /* Here xt looks like: ... */ if (xpath_vec(x0t, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) goto done; @@ -703,6 +777,7 @@ xmldb_get_cache(clicon_handle h, goto done; xml_spec_set(x1t, xml_spec(x0t)); + if (xlen < 1000){ /* This is optimized for the case when the tree is large and xlen is small * If the tree is large and xlen too, then the other is better. @@ -786,6 +861,7 @@ xmldb_get_zerocopy(clicon_handle h, db_elmnt *de = NULL; db_elmnt de0 = {0,}; int ret; + int empty = 0; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); @@ -794,7 +870,7 @@ xmldb_get_zerocopy(clicon_handle h, de = clicon_db_elmnt_get(h, db); if (de == NULL || de->de_xml == NULL){ /* Cache miss, read XML from file */ /* If there is no xml x0 tree (in cache), then read it from file */ - if ((ret = xmldb_readfile(h, db, yb, yspec, &x0t, msdiff)) < 0) + if ((ret = xmldb_readfile(h, db, yb, yspec, &x0t, &de0, msdiff)) < 0) goto done; if (ret == 0) goto fail; @@ -806,6 +882,25 @@ xmldb_get_zerocopy(clicon_handle h, } /* x0t == NULL */ else x0t = de->de_xml; + /* Check if empty, must be before add global defaults */ + if (xml_child_nr(x0t) == 0) + empty = 1; + + /* Add global defaults. + * Cant do it to x1t since that is after xpath check, since globals may be filtered out + */ + if (xml_default_yspec(yspec, x0t) < 0) + goto done; + /* If empty database, then disable NACM if loaded + * This has some drawbacks. One is that a config may become empty at a later stage + * and then this does not hold. + */ + if (empty && + clicon_option_bool(h, "CLICON_NACM_DISABLED_ON_EMPTY")){ + if (disable_nacm_on_empty(x0t, yspec) < 0) + goto done; + } + /* Here xt looks like: ... */ if (xpath_vec(x0t, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) goto done; @@ -874,7 +969,7 @@ xmldb_get(clicon_handle h, * @param[in] xpath String with XPATH syntax. or NULL for all * @param[in] copy Force copy. Overrides cache_zerocopy -> cache * @param[out] xret Single return XML tree. Free with xml_free() - * @param[out] msdiff If set, return modules-state differences (upgrade code) + * @param[out] msdiff If set, return modules-state differences (upgrade code) * @retval 0 OK * @retval -1 Error * @code @@ -944,12 +1039,16 @@ xmldb_get0_clear(clicon_handle h, if (x == NULL) goto ok; - /* clear XML tree of defaults */ + /* Mark non-presence containers as XML_FLAG_DEFAULT */ + if (xml_apply(x, CX_ELMNT, xml_nopresence_default_mark, (void*)XML_FLAG_DEFAULT) < 0) + goto done; + /* Clear XML tree of defaults */ if (xml_tree_prune_flagged(x, XML_FLAG_DEFAULT, 1) < 0) goto done; + /* clear mark and change */ xml_apply0(x, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, - (void*)(0xff)); + (void*)(0xffff)); ok: retval = 0; done: @@ -972,3 +1071,4 @@ xmldb_get0_free(clicon_handle h, *xp = NULL; return 0; } + diff --git a/lib/src/clixon_datastore_read.h b/lib/src/clixon_datastore_read.h index 3c87fe37..da182e57 100644 --- a/lib/src/clixon_datastore_read.h +++ b/lib/src/clixon_datastore_read.h @@ -39,6 +39,7 @@ /* * Prototypes */ -int xmldb_readfile(clicon_handle h, const char *db, yang_bind yb, yang_stmt *yspec, cxobj **xp, modstate_diff_t *msd); +int xmldb_readfile(clicon_handle h, const char *db, yang_bind yb, yang_stmt *yspec, + cxobj **xp, db_elmnt *de, modstate_diff_t *msd); #endif /* _CLIXON_DATASTORE_READ_H */ diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index 1044096b..ba1e45aa 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -481,9 +481,11 @@ text_modify(clicon_handle h, switch(op){ case OP_CREATE: if (x0){ - if (netconf_data_exists(cbret, "Data already exists; cannot create new resource") < 0) - goto done; - goto fail; + if (xml_nopresence_default(x0) == 0){ + if (netconf_data_exists(cbret, "Data already exists; cannot create new resource") < 0) + goto done; + goto fail; + } } case OP_REPLACE: /* fall thru */ case OP_MERGE: @@ -928,7 +930,7 @@ xmldb_put(clicon_handle h, /* If there is no xml x0 tree (in cache), then read it from file */ if (x0 == NULL){ firsttime++; /* to avoid leakage on error, see fail from text_modify */ - if ((ret = xmldb_readfile(h, db, YB_MODULE, yspec, &x0, NULL)) < 0) + if ((ret = xmldb_readfile(h, db, YB_MODULE, yspec, &x0, de, NULL)) < 0) goto done; if (ret == 0) goto fail; @@ -987,10 +989,10 @@ xmldb_put(clicon_handle h, db_elmnt de0 = {0,}; if (de != NULL) de0 = *de; - if (de0.de_xml == NULL){ + if (de0.de_xml == NULL) de0.de_xml = x0; - clicon_db_elmnt_set(h, db, &de0); - } + de0.de_empty = (xml_child_nr(de0.de_xml) == 0); + clicon_db_elmnt_set(h, db, &de0); } if (xmldb_db2file(h, db, &dbfile) < 0) goto done; @@ -1057,6 +1059,9 @@ xmldb_dump(clicon_handle h, char *format; int pretty; + /* clear XML tree of defaults */ + if (xml_tree_prune_flagged(xt, XML_FLAG_DEFAULT, 1) < 0) + goto done; /* Add modstate first */ if ((x = clicon_modst_cache_get(h, 1)) != NULL){ if ((xmodst = xml_dup(x)) == NULL) @@ -1079,3 +1084,4 @@ xmldb_dump(clicon_handle h, done: return retval; } + diff --git a/lib/src/clixon_nacm.c b/lib/src/clixon_nacm.c index fb5fab70..b9fc32fa 100644 --- a/lib/src/clixon_nacm.c +++ b/lib/src/clixon_nacm.c @@ -1100,7 +1100,9 @@ nacm_access(clicon_handle h, /* Do initial nacm processing common to all access validation in * RFC8341 3.4 */ /* 1. If the "enable-nacm" leaf is set to "false", then the protocol - operation is permitted. */ + * operation is permitted. + * note option CLICON_NACM_DISABLED_ON_EMPTY + */ if ((x = xpath_first(xnacm, nsc, "enable-nacm")) == NULL) goto permit; enabled = xml_body(x); @@ -1155,6 +1157,7 @@ nacm_access_pre(clicon_handle h, cxobj *xnacm = NULL; cvec *nsc = NULL; + /* Check clixon option: disabled, external tree or internal */ mode = clicon_option_str(h, "CLICON_NACM_MODE"); if (mode == NULL) goto permit; diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 712b32d0..f4333a5a 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -1883,6 +1883,7 @@ xml_copy_one(cxobj *x0, default: break; } + xml_flag_set(x1, xml_flag(x0, XML_FLAG_DEFAULT)); /* Maybe more flags */ retval = 0; done: return retval; diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 8e7e6461..1e976217 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -979,9 +979,223 @@ xml_namespace_change(cxobj *x, return retval; } -/*! Add default values (if not set) - * @param[in] xt XML tree with some node marked - * Typically called in a recursive apply function: +int +xml_default_create1(yang_stmt *y, + cxobj *xt, + int top, + cxobj **xcp) +{ + int retval = -1; + char *namespace; + char *prefix; + int ret; + cxobj *xc = NULL; + + if ((xc = xml_new(yang_argument_get(y), NULL, CX_ELMNT)) == NULL) + goto done; + xml_spec_set(xc, y); + + /* assign right prefix */ + if ((namespace = yang_find_mynamespace(y)) != NULL){ + prefix = NULL; + if ((ret = xml2prefix(xt, namespace, &prefix)) < 0) + goto done; + if (ret){ /* Namespace found, prefix returned in prefix */ + if (xml_prefix_set(xc, prefix) < 0) + goto done; + } + else{ /* Namespace does not exist in target, must add it w xmlns attr. + use source prefix */ + if (!top){ + if ((prefix = yang_find_myprefix(y)) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + } + if (add_namespace(xc, xc, prefix, namespace) < 0) + goto done; + /* Add prefix to x, if any */ + if (prefix && xml_prefix_set(xc, prefix) < 0) + goto done; + } + } + if (xml_addsub(xt, xc) < 0) + goto done; + *xcp = xc; + retval = 0; + done: + return retval; +} + +/*! Create leaf from default value + * + * @param[in] yt Yang spec + * @param[in] xt XML tree + * @param[in] top Use default namespace (if you create xmlns statement) + * @retval 0 OK + * @retval -1 Error + */ +static int +xml_default_create(yang_stmt *y, + cxobj *xt, + int top) +{ + int retval = -1; + cxobj *xc = NULL; + cxobj *xb; + char *str; + + if (xml_default_create1(y, xt, top, &xc) < 0) + goto done; + xml_flag_set(xc, XML_FLAG_DEFAULT); + if ((xb = xml_new("body", xc, CX_BODY)) == NULL) + goto done; + if ((str = cv2str_dup(yang_cv_get(y))) == 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; +} + +/*! Try to see if intermediate nodes are necessary for default values, create if so + * + * @param[in] yt Yang container (no-presence) + * @param[out] createp Need to create XML container + * @retval 0 OK + * @retval -1 Error + */ +static int +xml_nopresence_try(yang_stmt *yt, + int *createp) +{ + int retval = -1; + yang_stmt *y; + + if (yt == NULL || yang_keyword_get(yt) != Y_CONTAINER){ + clicon_err(OE_XML, EINVAL, "yt argument is not container"); + goto done; + } + *createp = 0; + y = NULL; + while ((y = yn_each(yt, y)) != NULL) { + switch (yang_keyword_get(y)){ + case Y_LEAF: + if (!cv_flag(yang_cv_get(y), V_UNSET)){ /* Default value exists */ + /* Need to create container */ + *createp = 1; + goto ok; + } + break; + case Y_CONTAINER: + if (yang_find(y, Y_PRESENCE, NULL) == NULL){ + /* If this is non-presence, (and it does not exist in xt) call recursively + * and create nodes if any default value exist first. Then continue and populate? + */ + if (xml_nopresence_try(y, createp) < 0) + goto done; + if (*createp) + goto ok; + } + break; + default: + break; + } /* switch */ + } + ok: + retval = 0; + done: + return retval; +} + +/*! Ensure default values are set on (children of) one single xml node + * + * Not recursive, except in one case with one or several non-presence containers, in which case + * XML containers may be created to host default values. That code may be a little too recursive. + * @param[in] yt Yang spec + * @param[in] xt XML tree (with yt as spec of xt, informally) + * @retval 0 OK + * @retval -1 Error + */ +static int +xml_default1(yang_stmt *yt, + cxobj *xt) +{ + int retval = -1; + yang_stmt *yc; + cxobj *xc; + int top=0; /* Top symbol (set default namespace) */ + int create = 0; + + if (xt == NULL){ /* No xml */ + clicon_err(OE_XML, EINVAL, "No XML argument"); + goto done; + } + switch (yang_keyword_get(yt)){ + case Y_MODULE: + case Y_SUBMODULE: + top++; + case Y_CONTAINER: /* XXX maybe check for non-presence here as well */ + case Y_LIST: + case Y_INPUT: + case Y_OUTPUT: + yc = NULL; + while ((yc = yn_each(yt, yc)) != NULL) { + switch (yang_keyword_get(yc)){ + case Y_LEAF: + if (!cv_flag(yang_cv_get(yc), V_UNSET)){ /* Default value exists */ + if (xml_find_type(xt, NULL, yang_argument_get(yc), CX_ELMNT) == NULL){ + /* No such child exist, create this leaf */ + if (xml_default_create(yc, xt, top) < 0) + goto done; + xml_sort(xt); + } + } + break; + case Y_CONTAINER: + if (yang_find(yc, Y_PRESENCE, NULL) == NULL){ + /* If this is non-presence, (and it does not exist in xt) call + * recursively and create nodes if any default value exist first. + * Then continue and populate? + */ + if (xml_find_type(xt, NULL, yang_argument_get(yc), CX_ELMNT) == NULL){ + /* No such container exist, recursively try if needed */ + if (xml_nopresence_try(yc, &create) < 0) + goto done; + if (create){ + /* Retval shows there is a default value need to create the + * container */ + if (xml_default_create1(yc, xt, top, &xc) < 0) + goto done; + xml_sort(xt); + /* Then call it recursively */ + if (xml_default1(yc, xc) < 0) + goto done; + } + } + } + break; + default: + break; + } + } + break; + default: + break; + } /* switch */ + retval = 0; + done: + return retval; +} + +/*! Ensure default values are set on existing leaf children of this node + * + * Assume yang is bound to the tree + * @param[in] xt XML tree * @retval 0 OK * @retval -1 Error */ @@ -990,79 +1204,22 @@ xml_default(cxobj *xt) { int retval = -1; yang_stmt *ys; - yang_stmt *y; - // int i; // XXX - cxobj *xc; - cxobj *xb; - char *str; - int added=0; - char *namespace; - char *prefix; - int ret; if ((ys = (yang_stmt*)xml_spec(xt)) == NULL){ retval = 0; goto done; } - /* Check leaf defaults */ - if (yang_keyword_get(ys) == Y_CONTAINER || yang_keyword_get(ys) == Y_LIST || - yang_keyword_get(ys) == Y_INPUT){ - y = NULL; - while ((y = yn_each(ys, y)) != NULL) { - if (yang_keyword_get(y) != Y_LEAF) - continue; - if (!cv_flag(yang_cv_get(y), V_UNSET)){ /* Default value exists */ - if (!xml_find(xt, yang_argument_get(y))){ - if ((xc = xml_new(yang_argument_get(y), NULL, CX_ELMNT)) == NULL) - goto done; - xml_spec_set(xc, y); - - /* assign right prefix */ - if ((namespace = yang_find_mynamespace(y)) != NULL){ - prefix = NULL; - if ((ret = xml2prefix(xt, namespace, &prefix)) < 0) - goto done; - if (ret){ - if (xml_prefix_set(xc, prefix) < 0) - goto done; - } - else{ /* namespace does not exist in target, use source prefix */ - if ((prefix = yang_find_myprefix(y)) == NULL){ - clicon_err(OE_UNIX, errno, "strdup"); - goto done; - } - if (add_namespace(xc, xc, prefix, namespace) < 0) - goto done; - /* Add prefix to x, if any */ - if (prefix && xml_prefix_set(xc, prefix) < 0) - goto done; - } - } - - xml_flag_set(xc, XML_FLAG_DEFAULT); - if ((xb = xml_new("body", xc, CX_BODY)) == NULL) - goto done; - if ((str = cv2str_dup(yang_cv_get(y))) == NULL){ - clicon_err(OE_UNIX, errno, "cv2str_dup"); - goto done; - } - if (xml_value_set(xb, str) < 0) - goto done; - free(str); - added++; - if (xml_insert(xt, xc, INS_LAST, NULL, NULL) < 0) - goto done; - } - } - } - } + if (xml_default1(ys, xt) < 0) + goto done; retval = 0; done: return retval; } -/*! Recursively fill in default values in a tree - * Alt to use xml_apply +/*! Recursively fill in default values in an XML tree + * @param[in] xt XML tree + * @retval 0 OK + * @retval -1 Error */ int xml_default_recurse(cxobj *xn) @@ -1082,6 +1239,85 @@ xml_default_recurse(cxobj *xn) return retval; } +/*! Ensure default values are set on top-level + * + * Not recursive, except in one case with one or several non-presence containers + * @param[in] xt XML tree + * Typically called in a recursive apply function: + * @retval 0 OK + * @retval -1 Error + */ +int +xml_default_yspec(yang_stmt *yspec, + cxobj *xt) +{ + int retval = -1; + yang_stmt *ymod = NULL; + + if (yspec == NULL || yang_keyword_get(yspec) != Y_SPEC){ + clicon_err(OE_XML, EINVAL, "yspec argument is not yang spec"); + goto done; + } + while ((ymod = yn_each(yspec, ymod)) != NULL) + if (xml_default1(ymod, xt) < 0) + goto done; + + retval = 0; + done: + return retval; +} + +/*! This node is a default set value or (recursively) a non-presence container + * @retval 1 xt is a nopresence/default node (ie "virtual") + * @retval 0 xt is not such a node + */ +int +xml_nopresence_default(cxobj *xt) +{ + cxobj *xc; + yang_stmt *yt; + + if ((yt = xml_spec(xt)) == NULL) + return 0; + switch (yang_keyword_get(yt)){ + case Y_CONTAINER: + if (yang_find(yt, Y_PRESENCE, NULL)) + return 0; + break; + case Y_LEAF: + return xml_flag(xt, XML_FLAG_DEFAULT)?1:0; + break; + default: + return 0; + } + xc = NULL; + while ((xc = xml_child_each(xt, xc, CX_ELMNT)) != NULL) { + if (xml_nopresence_default(xc) == 0) + return 0; + } + return 1; +} + +/*! Remove xml container if it is non-presence and only contains default leafs + * Called from xml_apply. Reason for marking is to delete it afterwords. + * @param[in] x + * @param[in] arg (flag value) + * @code + * if (xml_apply(xt, CX_ELMNT, xml_nopresence_default_mark, (void*)XML_FLAG_DEFAULT) < 0) + * err; + * if (xml_tree_prune_flagged(xt, XML_FLAG_DEFAULT, 1) < 0) + * goto done; + * @endcode + */ +int +xml_nopresence_default_mark(cxobj *x, + void *arg) +{ + if (xml_nopresence_default(x)) + xml_flag_set(x, (intptr_t)arg); + return 0; +} + /*! Sanitize an xml tree: xml node has matching yang_stmt pointer * @param[in] xt XML top of tree */ diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index 87289a69..c3182e82 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -1029,6 +1029,7 @@ xml_insert2(cxobj *xp, * @retval 0 OK * @retval -1 Error * @see xml_addsub where xc is appended. xml_insert is xml_addsub();xml_sort() + * @note It is assumed that all siblings of xi are YANG bound */ int xml_insert(cxobj *xp, diff --git a/test/test_nacm.sh b/test/test_nacm.sh index 23a8c041..540f2e49 100755 --- a/test/test_nacm.sh +++ b/test/test_nacm.sh @@ -2,7 +2,9 @@ # Authentication and authorization and IETF NACM # See RFC 8341 A.2 # But replaced ietf-netconf-monitoring with * -# Note credenials check set to none since USER poses as different users. +# Note: +# 1. credenials check set to none since USER poses as different users. +# 2. CLICON_NACM_DISABLE_ON_EMPTY: start with empty config and add nacm config # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -31,6 +33,7 @@ cat < $cfg /usr/local/var/$APPNAME false internal + true EOF @@ -140,7 +143,7 @@ expectpart "$(curl -u andy:bar $CURLOPTS -X GET $RCPROTO://localhost/restconf/da # explicitly disable nacm (regression on netgate bug) new "disable nacm" -expectpart "$(curl -u andy:bar $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": false}' $RCPROTO://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar $CURLOPTS -X PUT -H "Content-Type: application/yang-data+json" -d '{"ietf-netconf-acm:enable-nacm": false}' $RCPROTO://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "HTTP/1.1 204 No Content" new "auth set authentication config" expecteof "$clixon_netconf -qf $cfg" 0 "$RULES]]>]]>" "^]]>]]>$" diff --git a/test/test_nacm_credentials.sh b/test/test_nacm_credentials.sh index c470a380..b0b0189f 100755 --- a/test/test_nacm_credentials.sh +++ b/test/test_nacm_credentials.sh @@ -123,6 +123,7 @@ cat < $cfg /usr/local/var/$APPNAME/$APPNAME.pidfile /usr/local/var/$APPNAME internal + true $mode EOF diff --git a/test/test_nacm_datanode.sh b/test/test_nacm_datanode.sh index ea680008..7a90b44c 100755 --- a/test/test_nacm_datanode.sh +++ b/test/test_nacm_datanode.sh @@ -62,6 +62,7 @@ cat < $cfg /usr/local/var/$APPNAME false internal + true EOF diff --git a/test/test_nacm_datanode_read.sh b/test/test_nacm_datanode_read.sh index 98332ac1..3b9e915c 100755 --- a/test/test_nacm_datanode_read.sh +++ b/test/test_nacm_datanode_read.sh @@ -42,6 +42,7 @@ cat < $cfg /usr/local/var/$APPNAME false internal + true EOF diff --git a/test/test_nacm_datanode_write.sh b/test/test_nacm_datanode_write.sh index 83fb5efb..9d0cd2a6 100755 --- a/test/test_nacm_datanode_write.sh +++ b/test/test_nacm_datanode_write.sh @@ -34,6 +34,7 @@ cat < $cfg /usr/local/var/$APPNAME false internal + true EOF diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index f56c5db4..88b485c4 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -30,6 +30,7 @@ cat < $cfg $dir false internal + true $format EOF @@ -62,7 +63,6 @@ EOF # 6: expected return value of test2 # 7: expected return value of test3 # 8: startup mode: startup or init -# 9: Dont set default values (nullify them) testrun(){ enablenacm=$1 readdefault=$2 @@ -72,24 +72,9 @@ testrun(){ ret2=$6 ret3=$7 db=$8 - nulldef=$9 - # Set default values (or not) - if [ $nulldef -ne 0 ]; then - # Defaults should be: true permit deny permit: - # nacm enabled, exec default permit, read permit (expect fail)" - # which means results should be 0 1 3 - # Also enable-nacm is present since otherwise the nacm container would be removed - # since it is non-presence - NACM=$(cat < - ${enablenacm} - -EOF -) - else - NACM=$(cat < + NACM=$(cat < ${enablenacm} ${readdefault} ${writedefault} @@ -98,7 +83,6 @@ EOF EOF ) - fi # Initial data XML='42' @@ -117,6 +101,7 @@ EOF start_backend -s $db -f $cfg else new "Restart backend as eg follows: -Ff $cfg -s $db" + sleep 2 fi new "waiting" @@ -132,12 +117,16 @@ EOF wait_restconf # Use POST (instead of startup) + # Note this only works because CLICON_NACM_DISABLED_ON_EMPTY is true if [ $db = init ]; then - new "Set Initial data using POST" - expectpart "$(curl -u guest:bar $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d "$XML" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" + # Must set NACM first + new "Set NACM using PATCH" + expectpart "$(curl -u guest:bar $CURLOPTS -X PUT -H "Content-Type: application/yang-data+xml" -d "$NACM$XML" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" + +# new "Set Initial data using POST" +# expectpart "$(curl -u guest:bar $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d "$XML" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" - new "Set NACM using POST" - expectpart "$(curl -u guest:bar $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d "$NACM" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 201 Created" + fi #----------- First get @@ -204,39 +193,36 @@ EOF # Run a lot of tests with different settings of default read/write/exec # Outer loop either starts from startup or inits config via restconf POST for db in startup init; do - new "nacm enabled and all defaults permit" - testrun true permit permit permit 0 0 0 $db 0 + new "1. nacm enabled and all defaults permit" + testrun true permit permit permit 0 0 0 $db - new "nacm disabled and all defaults permit" - testrun false permit permit permit 0 0 0 $db 0 + new "2. nacm disabled and all defaults permit" + testrun false permit permit permit 0 0 0 $db - new "nacm disabled and all defaults deny" - testrun false deny deny deny 0 0 0 $db 0 + new "3. nacm disabled and all defaults deny" + testrun false deny deny deny 0 0 0 $db - new "nacm enabled, all defaults deny (expect fail)" - testrun true deny deny deny 1 1 1 $db 0 + new "4. nacm enabled, all defaults deny (expect fail)" + testrun true deny deny deny 1 1 1 $db - new "nacm enabled, exec default deny - read permit (expect fail)" - testrun true permit deny deny 1 1 1 $db 0 + new "5. nacm enabled, exec default deny - read permit (expect fail)" + testrun true permit deny deny 1 1 1 $db - new "nacm enabled, exec default deny - write permit (expect fail)" - testrun true deny permit deny 1 1 1 $db 0 + new "6. nacm enabled, exec default deny - write permit (expect fail)" + testrun true deny permit deny 1 1 1 $db - new "nacm enabled, exec default deny read/write permit (expect fail)" - testrun true permit permit deny 1 1 1 $db 0 + new "7. nacm enabled, exec default deny read/write permit (expect fail)" + testrun true permit permit deny 1 1 1 $db - new "nacm enabled, exec default permit, all others deny (expect fail)" - testrun true deny deny permit 2 1 2 $db 0 + new "8. nacm enabled, exec default permit, all others deny (expect fail)" + testrun true deny deny permit 2 1 2 $db - new "nacm enabled, exec default permit, read permit (expect fail)" - testrun true permit deny permit 0 1 3 $db 0 # This is yang default + new "9. nacm enabled, exec default permit, read permit (expect fail)" + testrun true permit deny permit 0 1 3 $db # This is yang default - new "nacm enabled, with default values (no settings - should be same as previous)" - # note last 1 means nullify all default values) - testrun true xxx xxx xxx 0 1 3 init 1 + new "10. nacm enabled, exec default permit, write permit (expect fail)" + testrun true deny permit permit 2 0 2 $db - new "nacm enabled, exec default permit, write permit (expect fail)" - testrun true deny permit permit 2 0 2 $db 0 done rm -rf $dir diff --git a/test/test_nacm_ext.sh b/test/test_nacm_ext.sh index a5606c60..19c5a60c 100755 --- a/test/test_nacm_ext.sh +++ b/test/test_nacm_ext.sh @@ -48,7 +48,8 @@ module nacm-example{ } prefix nacm; container authentication { - description "Example code for enabling www basic auth and some example + presence "To keep this from auto-expanding"; + description "Example code for enabling www basic auth and some example users"; leaf basic_auth{ description "Basic user / password authentication as in HTTP basic auth"; diff --git a/test/test_nacm_module_read.sh b/test/test_nacm_module_read.sh index da9c888e..4d7a9f2b 100755 --- a/test/test_nacm_module_read.sh +++ b/test/test_nacm_module_read.sh @@ -35,6 +35,7 @@ cat < $cfg false internal none + true EOF diff --git a/test/test_nacm_module_write.sh b/test/test_nacm_module_write.sh index 707fd20d..ecffa7f1 100755 --- a/test/test_nacm_module_write.sh +++ b/test/test_nacm_module_write.sh @@ -48,6 +48,7 @@ cat < $cfg /usr/local/var/$APPNAME false internal + true EOF diff --git a/test/test_nacm_protocol.sh b/test/test_nacm_protocol.sh index a55b456b..bc072b75 100755 --- a/test/test_nacm_protocol.sh +++ b/test/test_nacm_protocol.sh @@ -52,6 +52,7 @@ cat < $cfg false internal none + true EOF diff --git a/test/test_perf_mem.sh b/test/test_perf_mem.sh index 941ee377..4c5f9ee4 100755 --- a/test/test_perf_mem.sh +++ b/test/test_perf_mem.sh @@ -27,9 +27,6 @@ module scaling{ yang-version 1.1; namespace "urn:example:clixon"; prefix ex; - import "clixon-config" { - prefix cc; - } container x { list y { key "a"; diff --git a/test/test_restconf_patch.sh b/test/test_restconf_patch.sh index 052c93f8..d4181b36 100755 --- a/test/test_restconf_patch.sh +++ b/test/test_restconf_patch.sh @@ -26,6 +26,7 @@ cat < $cfg /usr/local/var/$APPNAME $dir internal + true EOF diff --git a/test/test_startup.sh b/test/test_startup.sh index b27cd3fe..b197d683 100755 --- a/test/test_startup.sh +++ b/test/test_startup.sh @@ -40,7 +40,6 @@ cat < $cfg init $format - EOF # Create running-db containin the interface "run" OK diff --git a/test/test_upgrade_quit.sh b/test/test_upgrade_quit.sh index dce92d99..e5575d6c 100755 --- a/test/test_upgrade_quit.sh +++ b/test/test_upgrade_quit.sh @@ -40,6 +40,11 @@ module interfaces{ reference "RFC 2863: The Interfaces Group MIB"; } + leaf foo{ + description "Should not appear"; + type string; + default "bar"; + } container interfaces { description "Interface configuration parameters."; @@ -52,8 +57,11 @@ module interfaces{ leaf description { type string; } - - + leaf foo{ + description "Should not appear"; + type string; + default "bar"; + } leaf type { type string; mandatory true; @@ -124,6 +132,11 @@ module interfaces{ reference "RFC 2863: The Interfaces Group MIB"; } + leaf foo{ + description "Should not appear"; + type string; + default "fie"; + } container interfaces { description "Interface configuration parameters."; @@ -133,6 +146,11 @@ module interfaces{ leaf name { type string; } + leaf foo{ + description "Should not appear"; + type string; + default "bar"; + } container docs{ description "Original description is wrapped and renamed"; leaf descr { @@ -290,7 +308,6 @@ XML='e0 ALL="$MODSTATE$XML" # -u means trigger example upgrade -new "test params: -s startup -f $cfg -- -u" # kill old backend (if any) new "kill old backend" @@ -301,6 +318,7 @@ fi new "start backend -s startup -f $cfg -q -- -u" output=$(sudo $clixon_backend -F -D $DBG -s startup -f $cfg -q -- -u) #echo "$output" + if [ "$ALL" != "$output" ]; then err "$ALL" "$output" fi diff --git a/yang/clixon/clixon-config@2020-06-17.yang b/yang/clixon/clixon-config@2020-06-17.yang index 70b2a00f..3ae88cd9 100644 --- a/yang/clixon/clixon-config@2020-06-17.yang +++ b/yang/clixon/clixon-config@2020-06-17.yang @@ -46,9 +46,8 @@ module clixon-config { description "Added: CLICON_CLI_LINES_DEFAULT Added enum HIDE to CLICON_CLI_GENMODEL - Added CLICON_SSL_SERVER_CERT - Added CLICON_SSL_SERVER_KEY - Added CLICON_SSL_CA_CERT"; + Added CLICON_SSL_SERVER_CERT, CLICON_SSL_SERVER_KEY, CLICON_SSL_CA_CERT + Added CLICON_NACM_DISABLED_ON_EMPTY"; } revision 2020-04-23 { description @@ -719,6 +718,20 @@ module clixon-config { exact for example, this user must exist and be used, otherwise another user (such as root or www) can pose as it."; } + leaf CLICON_NACM_DISABLED_ON_EMPTY { + type boolean; + default false; + description + "RFC 8341 and ietf-netconf-acm@2018-02-14.yang defines enable-nacm as true by + default. Since also write-default is deny by default it leads to that empty + configs can not be edited. + This means that a startup config must always have a NACM configuration or + that the NACM recovery session is used to edit an empty config. + If this option is set, Clixon disables NACM if a datastore is empty on load. + Note that it only makes the check on initial load, not if a store 'becomes' + empty, but enables a clixon nacm system to start empty and add an NACM + config after boot."; + } leaf CLICON_MODULE_LIBRARY_RFC7895 { type boolean; default true;