diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a7f8219..7f9e91c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
## 3.10.0/4.0.0 (Upcoming)
### Major New features
+* Yang "unique" feature supported
+ * See RFC 7950 7.8.3
* Persistent CLI history: [Preserve CLI command history across sessions. The up/down arrows](https://github.com/clicon/clixon/issues/79)
* The design is similar to bash history:
* The CLI loads/saves its complete history to a file on entry and exit, respectively
diff --git a/README.md b/README.md
index 5d195282..e01bafcd 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,6 @@ Clixon follows:
However, the following YANG syntax modules are not implemented:
- deviation
- min/max-elements
-- unique
- action
- refine
- Yang extended Xpath functions: re-match, deref, derived-from, derived-from-or-self, enum-value, bit-is-set
diff --git a/lib/clixon/clixon_netconf_lib.h b/lib/clixon/clixon_netconf_lib.h
index 01a7ee2f..9a43bbb2 100644
--- a/lib/clixon/clixon_netconf_lib.h
+++ b/lib/clixon/clixon_netconf_lib.h
@@ -66,6 +66,7 @@ int netconf_operation_failed(cbuf *cb, char *type, char *message);
int netconf_operation_failed_xml(cxobj **xret, char *type, char *message);
int netconf_malformed_message(cbuf *cb, char *message);
int netconf_malformed_message_xml(cxobj **xret, char *message);
+int netconf_data_not_unique(cbuf *cb, cxobj *x, cvec *cvk);
int netconf_trymerge(cxobj *x, yang_stmt *yspec, cxobj **xret);
int netconf_module_load(clicon_handle h);
char *netconf_db_find(cxobj *xn, char *name);
diff --git a/lib/src/clixon_netconf_lib.c b/lib/src/clixon_netconf_lib.c
index 45956aa9..2dbb4d21 100644
--- a/lib/src/clixon_netconf_lib.c
+++ b/lib/src/clixon_netconf_lib.c
@@ -966,6 +966,48 @@ netconf_malformed_message_xml(cxobj **xret,
return retval;
}
+/*! Create Netconf data-not-unique error message according to RFC 7950 15.1
+ *
+ * A NETCONF operation would result in configuration data where a
+ * "unique" constraint is invalidated.
+ * @param[out] cb CLIgen buf. Error XML is written in this buffer
+ * @param[in] x List element containing duplicate
+ * @param[in] cvk List of comonents in x that are non-unique
+ * @see RFC7950 Sec 15.1
+ */
+int
+netconf_data_not_unique(cbuf *cb,
+ cxobj *x,
+ cvec *cvk)
+{
+ int retval = -1;
+ cg_var *cvi = NULL;
+ cxobj *xi;
+
+ if (cprintf(cb, ""
+ "protocol"
+ "operation-failed"
+ "data-not-unique"
+ "error"
+ "") < 0)
+ goto err;
+ while ((cvi = cvec_each(cvk, cvi)) != NULL){
+ if ((xi = xml_find(x, cv_string_get(cvi))) == NULL)
+ continue; /* ignore, shouldnt happen */
+ cprintf(cb, "");
+ clicon_xml2cbuf(cb, xi, 0, 0);
+ cprintf(cb, "");
+ }
+ if (cprintf(cb, "") <0)
+ goto err;
+ retval = 0;
+ done:
+ return retval;
+ err:
+ clicon_err(OE_XML, errno, "cprintf");
+ goto done;
+}
+
/*! Help function: merge - check yang - if error make netconf errmsg
* @param[in] x XML tree
* @param[in] yspec Yang spec
@@ -1006,6 +1048,7 @@ netconf_trymerge(cxobj *x,
return retval;
}
+
/*! Load ietf netconf yang module and set enabled features
* The features added are (in order):
* candidate (8.3)
diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c
index 59519da0..1504f203 100644
--- a/lib/src/clixon_xml_map.c
+++ b/lib/src/clixon_xml_map.c
@@ -633,6 +633,167 @@ check_mandatory(cxobj *xt,
goto done;
}
+/*! New elemnt last in list, check if already exists if sp return -1
+ * @param[in] vec Vector of existing entries (new is last)
+ * @param[in] i1 The new entry is placed at vec[i1]
+ * @param[in] vlen Lenght of entry
+ * @retval 0 OK, entry is unique
+ * @retval -1 Duplicate detected
+ * @note This is currently linear complexity. It could be improved by inserting new element sorted and binary search.
+ */
+static int
+check_insert_duplicate(char **vec,
+ int i1,
+ int vlen)
+{
+ int i;
+ int v;
+ char *b;
+
+ for (i=0; iy-unique - "a"
+ * xt->x -> ab
+ * x -> bc
+ * x -> ab
+ */
+static int
+check_unique_parent(cxobj *xt,
+ cbuf *cbret)
+{
+ int retval = -1;
+ cxobj *x = NULL;
+ yang_stmt *y;
+ yang_stmt *yp = NULL; /* previous in list */
+ yang_stmt *yu;
+ int ret;
+
+ /* */
+ while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) {
+ if ((y = xml_spec(x)) == NULL)
+ continue;
+ if (y == yp) /* If same yang as previous x, then skip (eg same list) */
+ continue;
+ yp = y;
+ if (yang_keyword_get(y) != Y_LIST)
+ continue;
+ yu = NULL;
+ while ((yu = yn_each(y, yu)) != NULL) {
+ if (yang_keyword_get(yu) != Y_UNIQUE)
+ continue;
+ /* Here is a list w unique constraints identified by:
+ * its first element x, its yang spec y, its parent xt, and
+ * a unique yang spec yu,
+ */
+ if ((ret = check_unique_list(x, xt, y, yu, 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 added entry
* 1. Check if mandatory leafs present as subs.
* 2. Check leaf values, eg int ranges and string regexps.
@@ -831,6 +992,10 @@ xml_yang_validate_all(cxobj *xt,
default:
break;
}
+ if ((ret = check_unique_parent(xt, cbret)) < 0)
+ goto done;
+ if (ret == 0)
+ goto fail;
/* must sub-node RFC 7950 Sec 7.5.3. Can be several.
* XXX. use yang path instead? */
yc = NULL;
diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c
index 525fc51b..f95db268 100644
--- a/lib/src/clixon_yang.c
+++ b/lib/src/clixon_yang.c
@@ -1213,7 +1213,6 @@ yang_print_cbuf(cbuf *cb,
return 0;
}
-
/*! Populate yang leafs after parsing. Create cv and fill it in.
*
* Populate leaf in 2nd round of yang parsing, now that context is complete:
@@ -1308,7 +1307,8 @@ ys_populate_list(yang_stmt *ys,
if ((ykey = yang_find(ys, Y_KEY, NULL)) == NULL)
return 0;
- cvec_free(ys->ys_cvec);
+ if (ys->ys_cvec)
+ cvec_free(ys->ys_cvec);
if ((ys->ys_cvec = yang_arg2cvec(ykey, " ")) == NULL)
return -1;
return 0;
@@ -1681,6 +1681,18 @@ ys_populate_feature(clicon_handle h,
return retval;
}
+/*! Populate the unique statement with a cvec
+ */
+static int
+ys_populate_unique(yang_stmt *ys)
+{
+ if (ys->ys_cvec)
+ cvec_free(ys->ys_cvec);
+ if ((ys->ys_cvec = yang_arg2cvec(ys, " ")) == NULL)
+ return -1;
+ return 0;
+}
+
/*! Populate unknown node with extension
*/
static int
@@ -1770,6 +1782,10 @@ ys_populate(yang_stmt *ys,
if (ys_populate_identity(ys, NULL) < 0)
goto done;
break;
+ case Y_UNIQUE:
+ if (ys_populate_unique(ys) < 0)
+ goto done;
+ break;
case Y_UNKNOWN:
if (ys_populate_unknown(ys) < 0)
goto done;
diff --git a/test/test_unique.sh b/test/test_unique.sh
new file mode 100755
index 00000000..799a51bb
--- /dev/null
+++ b/test/test_unique.sh
@@ -0,0 +1,226 @@
+#!/bin/bash
+# Yang list unique tests
+# Use example in RFC7890 7.8.3.1, modify fields and also test a variant with
+# a single unique identifier (rfc example has two)
+# The test adds the rfc conf that fails, then one that passes, then makes add
+# to fail it and then del to pass it.
+# Then makes a fail / pass test on the single field case
+# Last, a complex unsorted list with several sub-elements.
+
+# Magic line must be first in script (see README.md)
+s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
+
+APPNAME=example
+
+cfg=$dir/conf_yang.xml
+fyang=$dir/unique.yang
+
+cat < $cfg
+
+ $cfg
+ /usr/local/share/clixon
+ $fyang
+ /usr/local/lib/$APPNAME/clispec
+ /usr/local/lib/$APPNAME/cli
+ $APPNAME
+ /usr/local/var/$APPNAME/$APPNAME.sock
+ /usr/local/var/$APPNAME/$APPNAME.pidfile
+ 1
+ /usr/local/var/$APPNAME
+
+EOF
+
+# Example (the list server part) from RFC7950 Sec 7.8.3.1 w changed types
+cat < $fyang
+module unique{
+ yang-version 1.1;
+ namespace "urn:example:clixon";
+ prefix un;
+ container c{
+ leaf a{
+ type string;
+ }
+ list server {
+ description "RFC7950 7.8.3.1";
+ key "name";
+ unique "ip port";
+ leaf name {
+ type string;
+ }
+ leaf ip {
+ type string;
+ }
+ leaf port {
+ type uint16;
+ }
+ }
+ list other {
+ description "random inserted data";
+ key a;
+ leaf a{
+ type string;
+ }
+ }
+ list single {
+ description "similar with just a single unique field";
+ key "name";
+ unique "ip";
+ leaf name {
+ type string;
+ }
+ leaf ip {
+ type string;
+ }
+ }
+ leaf b{
+ type string;
+ }
+ }
+}
+EOF
+
+new "test params: -f $cfg"
+
+if [ $BE -ne 0 ]; then
+ new "kill old backend"
+ sudo clixon_backend -zf $cfg
+ if [ $? -ne 0 ]; then
+ err
+ fi
+ new "start backend -s init -f $cfg"
+ # start new backend
+ start_backend -s init -f $cfg
+
+ new "waiting"
+ sleep $RCWAIT
+fi
+
+# RFC test two-field caes
+new "Add not valid example"
+expecteof "$clixon_netconf -qf $cfg" 0 'replace
+ smtp
+ 192.0.2.1
+ 25
+
+
+ http
+ 192.0.2.1
+ 25
+
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate (should fail)"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^protocoloperation-faileddata-not-uniqueerror192.0.2.125]]>]]>$'
+
+new "netconf discard-changes"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+new "Add valid example"
+expecteof "$clixon_netconf -qf $cfg" 0 'replace
+ smtp
+ 192.0.2.1
+ 25
+
+
+ http
+ 192.0.2.1
+
+
+ ftp
+ 192.0.2.1
+
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate ok"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+new "make it invalid by adding port to ftp entry"
+expecteof "$clixon_netconf -qf $cfg" 0 'noneftp25
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate (should fail)"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^protocoloperation-faileddata-not-uniqueerror192.0.2.125]]>]]>$'
+
+new "make it valid by deleting port from smtp entry"
+expecteof "$clixon_netconf -qf $cfg" 0 'nonesmtp25
+]]>]]>' '^]]>]]>$'
+
+new "netconf validate ok"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+new "netconf discard-changes"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+# Then test single-field case
+new "Add not valid example"
+expecteof "$clixon_netconf -qf $cfg" 0 'replace
+ smtp
+ 192.0.2.1
+
+
+ http
+ 192.0.2.1
+
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate (should fail)"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^protocoloperation-faileddata-not-uniqueerror192.0.2.1]]>]]>$'
+
+new "make valid by replacing IP of http entry"
+expecteof "$clixon_netconf -qf $cfg" 0 'nonehttp178.23.34.1
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate ok"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+new "netconf discard-changes"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+# Then test composite case (detect duplicates among other elements)
+# and also unordered
+
+new "Add not valid example"
+expecteof "$clixon_netconf -qf $cfg" 0 'replace
+ other
+
+ smtp
+ 192.0.2.1
+
+ other
+
+ smtp
+ 192.0.2.1
+ 25
+
+
+ http
+ 192.0.2.1
+
+ xx
+
+ http
+ 192.0.2.1
+ 25
+
+]]>]]>' "^]]>]]>$"
+
+new "netconf validate (should fail)"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^protocoloperation-faileddata-not-uniqueerror192.0.2.125]]>]]>$'
+
+new "netconf discard-changes"
+expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$"
+
+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