From 0a20348b3be99799aad2cf749affdcc38aaf9f69 Mon Sep 17 00:00:00 2001 From: Jan-Olof Carlson Date: Sat, 23 Jul 2022 06:57:54 +0000 Subject: [PATCH] rfc6243 with-defaults Capability for NETCONF --- apps/backend/backend_get.c | 35 +++- lib/clixon/clixon_xml_map.h | 1 + lib/src/clixon_netconf_lib.c | 5 + lib/src/clixon_xml_map.c | 36 ++++ test/test_netconf_hello.sh | 2 +- test/test_netconf_ssh_callhome.sh | 2 +- test/test_yang_with_defaults.sh | 336 ++++++++++++++++++++++++++++++ 7 files changed, 411 insertions(+), 6 deletions(-) create mode 100755 test/test_yang_with_defaults.sh diff --git a/apps/backend/backend_get.c b/apps/backend/backend_get.c index 6b21605e..55c74162 100644 --- a/apps/backend/backend_get.c +++ b/apps/backend/backend_get.c @@ -721,7 +721,8 @@ get_common(clicon_handle h, int list_pagination = 0; cxobj **xvec = NULL; size_t xlen; - cxobj *xlistpag; + cxobj *xfind; + char *with_defaults; clicon_debug(1, "%s", __FUNCTION__); username = clicon_username_get(h); @@ -762,14 +763,14 @@ get_common(clicon_handle h, } } /* Check if list pagination */ - if ((xlistpag = xml_find_type(xe, NULL, "list-pagination", CX_ELMNT)) != NULL) + if ((xfind = xml_find_type(xe, NULL, "list-pagination", CX_ELMNT)) != NULL) list_pagination = 1; /* Sanity check for list pagination: path must be a list/leaf-list, if it is, * check config/state */ if (list_pagination){ if (get_list_pagination(h, ce, - xlistpag, + xfind, content, db, depth, yspec, xpath, nsc, username, cbret) < 0) @@ -842,7 +843,33 @@ get_common(clicon_handle h, } break; } - + /* rfc6243 Handle with-defaults. */ + if ((xfind = xml_find(xe, "with-defaults")) != NULL) { + if ((with_defaults = xml_find_value(xfind, "body")) != NULL) { + if (strcmp(with_defaults, "explicit") == 0) { + /* Traverse XML and mark state nodes */ + if (xml_non_config_data(xret, NULL) < 0) + goto done; + /* Remove default configuration nodes from XML */ + if (xml_tree_prune_flags(xret, XML_FLAG_DEFAULT, XML_FLAG_MARK|XML_FLAG_DEFAULT) < 0) + goto done; + } + else if (strcmp(with_defaults, "report-all") == 0) { + /* Accept mode, do nothing */ + } + else { + /* Mode not supported */ + if ((cbmsg = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(cbmsg, "with-defaults retrieval mode \"%s\" is not supported", with_defaults); + if (netconf_operation_failed(cbret, "application", cbuf_get(cbmsg)) < 0) + goto done; + goto ok; + } + } + } if (content != CONTENT_CONFIG && clicon_option_bool(h, "CLICON_VALIDATE_STATE_XML")){ /* Check XML by validating it. return internal error with error cause diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 434d3282..aa93d85d 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -59,6 +59,7 @@ int xml_diff(yang_stmt *yspec, cxobj *x0, cxobj *x1, cxobj ***changed_x0, cxobj ***changed_x1, int *changedlen); int xml_tree_prune_flagged_sub(cxobj *xt, int flag, int test, int *upmark); int xml_tree_prune_flagged(cxobj *xt, int flag, int test); +int xml_tree_prune_flags(cxobj *xt, int flags, int mask); int xml_namespace_change(cxobj *x, char *ns, char *prefix); int xml_default_recurse(cxobj *xn, int state); int xml_global_defaults(clicon_handle h, cxobj *xn, cvec *nsc, const char *xpath, yang_stmt *yspec, int state); diff --git a/lib/src/clixon_netconf_lib.c b/lib/src/clixon_netconf_lib.c index 8eae6d3a..9a7d0194 100644 --- a/lib/src/clixon_netconf_lib.c +++ b/lib/src/clixon_netconf_lib.c @@ -1529,6 +1529,9 @@ netconf_module_load(clicon_handle h) /* Load ietf list pagination netconf */ if (yang_spec_parse_module(h, "ietf-list-pagination-nc", NULL, yspec)< 0) goto done; + /* Load rfc6243 with-defaults module explicit (imported by ietf-list-pagination-nc) */ + if (yang_spec_parse_module(h, "ietf-netconf-with-defaults", NULL, yspec)< 0) + goto done; /* Framing: If hello protocol skipped, set framing direct, ie fix chunked framing if NETCONF-1.1 * But start with default: RFC 4741 EOM ]]>]]> * For now this only applies to external protocol @@ -1721,6 +1724,8 @@ netconf_hello_server(clicon_handle h, cprintf(cb, "urn:ietf:params:netconf:capability:startup:1.0"); cprintf(cb, "urn:ietf:params:netconf:capability:xpath:1.0"); cprintf(cb, "urn:ietf:params:netconf:capability:notification:1.0"); + /* rfc6243 with-defaults capability modes */ + cprintf(cb, "urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit"); cprintf(cb, ""); if (session_id) cprintf(cb, "%lu", (long unsigned int)session_id); diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 9a33f129..d7778194 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -561,6 +561,42 @@ xml_tree_prune_flagged(cxobj *xt, return retval; } +/*! Prune everything that passes test + * @param[in] xt XML tree with some node marked + * @param[in] flags Flags set + * @param[in] mask Which flags to test for + * The function removes all branches that does pass test + * @code + * xml_tree_prune_flaggs(xt, XML_FLAG_MARK, XML_FLAG_MARK|XML_FLAG_DEFAULT); + * @endcode + */ +int +xml_tree_prune_flags(cxobj *xt, + int flags, + int mask) +{ + int retval = -1; + cxobj *x; + cxobj *xprev; + + x = NULL; + xprev = NULL; + while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) { + if (xml_flag(x, mask) == flags){ /* Pass test means purge */ + if (xml_purge(x) < 0) + goto done; + x = xprev; + continue; + } + if (xml_tree_prune_flags(x, flags, mask) < 0) + goto done; + xprev = x; + } + retval = 0; + done: + return retval; +} + /*! Add prefix:namespace pair to xml node, set cache, etc * @param[in] x XML node whose namespace should change * @param[in] xp XML node where namespace attribute should be declared (can be same) diff --git a/test/test_netconf_hello.sh b/test/test_netconf_hello.sh index 0f91edb8..4b6e24fc 100755 --- a/test/test_netconf_hello.sh +++ b/test/test_netconf_hello.sh @@ -106,7 +106,7 @@ new "Netconf snd hello with prefix" expecteof "$clixon_netconf -qef $cfg" 0 "urn:ietf:params:netconf:base:1.1]]>]]>" '^$' new "netconf snd + rcv hello" -expecteof "$clixon_netconf -f $cfg" 0 "urn:ietf:params:netconf:base:1.1]]>]]>" "^urn:ietf:params:netconf:base:1.1urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:capability:yang-library:1.0?revision=2019-01-04&module-set-id=42urn:ietf:params:netconf:capability:candidate:1.0urn:ietf:params:netconf:capability:validate:1.1urn:ietf:params:netconf:capability:startup:1.0urn:ietf:params:netconf:capability:xpath:1.0urn:ietf:params:netconf:capability:notification:1.0[0-9]*]]>]]>$" '^$' +expecteof "$clixon_netconf -f $cfg" 0 "urn:ietf:params:netconf:base:1.1]]>]]>" "^urn:ietf:params:netconf:base:1.1urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:capability:yang-library:1.0?revision=2019-01-04&module-set-id=42urn:ietf:params:netconf:capability:candidate:1.0urn:ietf:params:netconf:capability:validate:1.1urn:ietf:params:netconf:capability:startup:1.0urn:ietf:params:netconf:capability:xpath:1.0urn:ietf:params:netconf:capability:notification:1.0urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit[0-9]*]]>]]>$" '^$' new "Netconf snd hello with extra element" expecteof "$clixon_netconf -qef $cfg" 0 "urn:ietf:params:netconf:base:1.1]]>]]>" '^protocolunknown-elementextra-elementerrorUnrecognized hello/capabilities element]]>]]>$' '^$' diff --git a/test/test_netconf_ssh_callhome.sh b/test/test_netconf_ssh_callhome.sh index b17f163e..4f7e2d98 100755 --- a/test/test_netconf_ssh_callhome.sh +++ b/test/test_netconf_ssh_callhome.sh @@ -132,7 +132,7 @@ EOF new "Start Listener client" echo "ssh -s -F $sshcfg -v -i $key -o ProxyUseFdpass=yes -o ProxyCommand=\"clixon_netconf_ssh_callhome_client -a 127.0.0.1\" . netconf" #-F $sshcfg -expectpart "$(ssh -s -F $sshcfg -v -i $key -o ProxyUseFdpass=yes -o ProxyCommand="${clixon_netconf_ssh_callhome_client} -a 127.0.0.1" . netconf < $rpccmd)" 0 "urn:ietf:params:netconf:base:1.1urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:capability:yang-library:1.0?revision=2019-01-04&module-set-id=42urn:ietf:params:netconf:capability:candidate:1.0urn:ietf:params:netconf:capability:validate:1.1urn:ietf:params:netconf:capability:startup:1.0urn:ietf:params:netconf:capability:xpath:1.0urn:ietf:params:netconf:capability:notification:1.02" "" +expectpart "$(ssh -s -F $sshcfg -v -i $key -o ProxyUseFdpass=yes -o ProxyCommand="${clixon_netconf_ssh_callhome_client} -a 127.0.0.1" . netconf < $rpccmd)" 0 "urn:ietf:params:netconf:base:1.1urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:capability:yang-library:1.0?revision=2019-01-04&module-set-id=42urn:ietf:params:netconf:capability:candidate:1.0urn:ietf:params:netconf:capability:validate:1.1urn:ietf:params:netconf:capability:startup:1.0urn:ietf:params:netconf:capability:xpath:1.0urn:ietf:params:netconf:capability:notification:1.0urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit2" "" # Wait wait diff --git a/test/test_yang_with_defaults.sh b/test/test_yang_with_defaults.sh new file mode 100755 index 00000000..4896f21a --- /dev/null +++ b/test/test_yang_with_defaults.sh @@ -0,0 +1,336 @@ +#!/usr/bin/env bash + + +# Test of the IETF rfc6243: With-defaults Capability for NETCONF +# +# Test cases below follows the RFC. +# + +# 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/example-default.yang +fstate=$dir/state.xml + +cat < $cfg + + $cfg + ietf-netconf:startup + 42 + ${YANG_INSTALLDIR} + $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 + $dir + false + +EOF + +# A.1. Example YANG Module +# The following YANG module defines an example interfaces table to +# demonstrate how the parameter behaves for a specific +# data model. +cat < $fyang +module example { + + namespace "http://example.com/ns/interfaces"; + + prefix exam; + + typedef status-type { + description "Interface status"; + type enumeration { + enum ok; + enum 'waking up'; + enum 'not feeling so good'; + enum 'better check it out'; + enum 'better call for help'; + } + default ok; + } + + container interfaces { + description "Example interfaces group"; + + list interface { + description "Example interface entry"; + key name; + + leaf name { + description + "The administrative name of the interface. + This is an identifier that is only unique + within the scope of this list, and only + within a specific server."; + type string { + length "1 .. max"; + } + } + + leaf mtu { + description + "The maximum transmission unit (MTU) value assigned to + this interface."; + type uint32; + default 1500; + } + + leaf status { + description + "The current status of this interface."; + type status-type; + config false; + } + } + } + } +EOF + +# A.2. Example Data Set + +EXAMPLENS="xmlns=\"http://example.com/ns/interfaces\"" + +XML="\ +eth08192\ +eth1\ +eth29000\ +eth31500\ +" + +cat < $fstate + +eth2not feeling so good +eth3waking up + +EOF + + +db=startup +if [ $db = startup ]; then + sudo echo "<${DATASTORE_TOP}>$XML" > $dir/startup_db +fi +if [ $BE -ne 0 ]; then # Bring your own backend + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -s $db -f $cfg" + start_backend -s $db -f $cfg -- -sS $fstate +fi + +new "wait backend" +wait_backend + +# permission kludges +new "chmod datastores" +sudo chmod 666 $dir/running_db +if [ $? -ne 0 ]; then + err1 "chmod $dir/running_db" +fi +sudo chmod 666 $dir/startup_db +if [ $? -ne 0 ]; then + err1 "chmod $dir/startup_db" +fi + +new "Checking startup unchanged" +ret=$(diff $dir/startup_db <(echo "<${DATASTORE_TOP}>$XML")) +if [ $? -ne 0 ]; then + err "<${DATASTORE_TOP}>$XML" "$ret" +fi + +new "Checking running unchanged" +ret=$(diff $dir/running_db <(echo -n "<${DATASTORE_TOP}>$XML")) +if [ $? -ne 0 ]; then + err "<${DATASTORE_TOP}>$XML" "$ret" +fi + +new "rfc4243 4.3. Capability Identifier" +expecteof "$clixon_netconf -ef $cfg" 0 "$DEFAULTHELLO" \ +"urn:ietf:params:netconf:capability:with-defaults:1.0?basic-mode=explicit" + +new "rfc6243 3.1. 'report-all' Retrieval Mode" +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +report-all" \ +"" \ +"\ +eth08192ok\ +eth11500ok\ +eth29000not feeling so good\ +eth31500waking up\ +" + +new "rfc6243 3.2. 'trim' Retrieval Mode" +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +trim" \ +"" \ +"applicationoperation-failed\ +error\ +with-defaults retrieval mode \"trim\" is not supported" + +new "rfc6243 3.3. 'explicit' Retrieval Mode" +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +explicit" \ +"" \ +"\ +eth08192ok\ +eth1ok\ +eth29000not feeling so good\ +eth31500waking up\ +" + +new "rfc6243 3.4. 'report-all-tagged' Retrieval Mode" +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +report-all-tagged" \ +"" \ +"applicationoperation-failed\ +error\ +with-defaults retrieval mode \"report-all-tagged\" is not supported" + +new "rfc6243 2.3.1. 'explicit' Basic Mode Retrieval" +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"" "" \ +"\ +eth08192ok\ +eth11500ok\ +eth29000not feeling so good\ +eth31500waking up\ +" "" + +new "rfc6243 2.3.3. 'explicit' and Behavior (part 1): create explicit node" +# A valid 'create' operation attribute for a data node that has +# been set by a client to its schema default value MUST fail with a +# 'data-exists' error-tag. +# (test: try to create mtu=3000 on interface eth3) +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +\ +eth33000\ +none " "" \ +"\ +application\ +data-exists\ +error\ +Data already exists; cannot create new resource\ +" +# nothing to commit here, but just to verify +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "" "" "" +# verify no change +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"" "" \ +"\ +eth08192ok\ +eth11500ok\ +eth29000not feeling so good\ +eth31500waking up\ +" "" + +new "rfc6243 2.3.3. 'explicit' and Behavior (part 2): create default node" +# A valid 'create' operation attribute for a +# data node that has been set by the server to its schema default value +# MUST succeed. +# (test: set mtu=3000 on interface eth1) +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +\ +eth13000\ +none " \ +"" \ +"" +# commit change +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "" "" "" +# verify that the mtu value has changed +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"" "" \ +"\ +eth08192ok\ +eth13000ok\ +eth29000not feeling so good\ +eth31500waking up\ +" "" + +new "rfc6243 2.3.3. 'explicit' and Behavior (part 3): delete explicit node" +# A valid 'delete' operation attribute for a data node +# that has been set by a client to its schema default value MUST +# succeed. +# (test: try to delete mtu on interface eth1) +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +\ +eth1\ +none" \ +"" \ +"" +# commit delete +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "" "" "" +# check thet the default mtu vale has been restored +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"" "" \ +"\ +eth08192ok\ +eth11500ok\ +eth29000not feeling so good\ +eth31500waking up\ +" "" + +new "rfc6243 2.3.3. 'explicit' and Behavior (part 4): delete default node" +# A valid 'delete' operation attribute for a data node that +# has been set by the server to its schema default value MUST fail with +# a 'data-missing' error-tag. +#(test: try to delete default mtu on interface eth1) +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"\ +\ +eth11500\ +none" \ +"" \ +"\ +application\ +data-missing\ +error\ +Data does not exist; cannot delete resource\ +" +# nothing to commit +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "" "" "" +# verify that the configuration has not changed +expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" \ +"" "" \ +"\ +eth08192ok\ +eth11500ok\ +eth29000not feeling so good\ +eth31500waking up\ +" "" + + +if [ $BE -ne 0 ]; then # Bring your own backend + new "Kill backend" + # Check if premature kill + pid=$(pgrep -u root -f clixon_backend) + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg +fi + +rm -rf $dir + +unset ret + +new "endtest" +endtest