From e7b60619daf9b164a3f06d1b13c910e060299b81 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 23 Jul 2019 22:11:14 +0200 Subject: [PATCH 01/10] * Pushed tag to 4.0.1.PRE * Restconf RFC 8040 increased feature compliance * Cache-Control: no-cache added in HTTP responses (RFC Section 5.5) * Restconf monitoring capabilities (RFC Section 9.1) * Added support for Yang extensions * New plugin callback: ca_extension * Main backend example includes example code on how to implement a Yang extension in a plugin. * JSON changes * Non-pretty-print output removed all extra spaces. * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` * Empty JSON container changed from `null` to `{}`. * Empty list and leafs remain as `null` * Removed unnecessary configure dependencies * libnsl, libcrypt, libm, if_vlan,... * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions * Yang Unique statements with multiple schema identifiers did not work on some platforms due to memory error. --- CHANGELOG.md | 27 +- README.md | 17 +- apps/backend/Makefile.in | 2 +- apps/backend/backend_client.c | 73 +++- apps/backend/backend_main.c | 14 +- apps/backend/backend_plugin.c | 17 - apps/backend/backend_plugin.h | 2 - apps/backend/backend_startup.c | 3 - apps/cli/cli_common.c | 3 - apps/cli/cli_main.c | 6 + apps/cli/cli_plugin.c | 6 - apps/cli/cli_show.c | 3 - apps/netconf/netconf_main.c | 12 +- apps/restconf/restconf_lib.c | 2 +- apps/restconf/restconf_main.c | 114 ++++-- apps/restconf/restconf_methods.c | 2 +- configure | 159 +------- configure.ac | 11 +- doc/FAQ.md | 1 + example/main/Makefile.in | 2 +- example/main/README.md | 28 ++ ...13.yang => clixon-example@2019-07-23.yang} | 23 +- example/main/example_backend.c | 37 ++ include/clixon_config.h.in | 15 - lib/clixon/clixon_json.h | 4 +- lib/clixon/clixon_plugin.h | 20 +- lib/clixon/clixon_yang.h | 3 +- lib/src/clixon_json.c | 24 +- lib/src/clixon_plugin.c | 99 ++++- lib/src/clixon_string.c | 2 +- lib/src/clixon_xml_map.c | 6 +- lib/src/clixon_yang.c | 145 ++++--- lib/src/clixon_yang_internal.h | 4 +- lib/src/clixon_yang_parse.l | 5 +- lib/src/clixon_yang_parse.y | 27 +- test/lib.sh | 115 +++++- test/test_choice.sh | 6 +- test/test_json.sh | 61 ++- test/test_nacm.sh | 20 +- test/test_nacm_default.sh | 16 +- test/test_nacm_ext.sh | 18 +- test/test_nacm_module_read.sh | 44 +-- test/test_nacm_module_write.sh | 28 +- test/test_nacm_protocol.sh | 10 +- test/test_perf_state.sh | 2 +- test/test_restconf.sh | 62 +-- test/test_restconf2.sh | 38 +- test/test_restconf_err.sh | 2 +- test/test_restconf_jukebox.sh | 364 ++++++++++++++++++ test/test_restconf_listkey.sh | 18 +- test/test_rpc.sh | 18 +- test/test_stream.sh | 4 +- test/test_submodule.sh | 2 +- test/test_xml.sh | 12 +- test/test_yang_extension.sh | 126 ++++++ test/test_yang_namespace.sh | 8 +- util/clixon_util_json.c | 11 +- yang/clixon/clixon-config@2019-06-05.yang | 5 +- yang/standard/Makefile.in | 1 + yang/standard/ietf-restconf@2017-01-26.yang | 278 +++++++++++++ 60 files changed, 1619 insertions(+), 568 deletions(-) rename example/main/{clixon-example@2019-01-13.yang => clixon-example@2019-07-23.yang} (85%) create mode 100755 test/test_restconf_jukebox.sh create mode 100755 test/test_yang_extension.sh create mode 100644 yang/standard/ietf-restconf@2017-01-26.yang diff --git a/CHANGELOG.md b/CHANGELOG.md index f49bf034..463ab2c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,31 @@ # Clixon Changelog -## 4.0.0 (Expected: 13 July 2019) +## 4.1.0 (Expected: August 2019) + +### Major New features +* Restconf RFC 8040 increased feature compliance + * Cache-Control: no-cache added in HTTP responses (RFC Section 5.5) + * Restconf monitoring capabilities (RFC Section 9.1) +* Added support for Yang extensions + * New plugin callback: ca_extension + * Main backend example includes example code on how to implement a Yang extension in a plugin. + +### API changes on existing features (you may need to change your code) +* JSON changes + * Non-pretty-print output removed all extra spaces. + * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` + * Empty JSON container changed from `null` to `{}`. + * Empty list and leafs remain as `null` + +### Minor changes +* Removed unnecessary configure dependencies + * libnsl, libcrypt, libm, if_vlan,... +* pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions + +### Corrected Bugs +* Yang Unique statements with multiple schema identifiers did not work on some platforms due to memory error. + +## 4.0.0 (13 July 2019) ### Summary diff --git a/README.md b/README.md index 47b72648..91c10429 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,6 @@ However, the following YANG syntax modules are not implemented (reference to RFC - require-instance - instance-identifier type - status (7.21.2) -- extension (7.19) supported syntactically, but no hooks/plugins for extenstions - YIN (13) - Yang extended Xpath functions: re-match(), deref)(), derived-from(), derived-from-or-self(), enum-value(), bit-is-set() (10.2-10.6) - Default values on leaf-lists are not supported (7.7.2) @@ -204,7 +203,6 @@ You can create namespace in three ways: * `xml_nsctx_node()` by copying an XML namespace context from an existing XML node. * `xml_nsctx_yang()` by computing an XML namespace context a yang module import statements. - ## Netconf Clixon implements the following NETCONF proposals or standards: @@ -308,17 +306,16 @@ The figure shows the SDK runtime of Clixon. ## Standard Compliance -This is work-in-progress on which standards Clixon supports: -- [RFC 6020](https://www.rfc-editor.org/rfc/rfc6020.txt) YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF) +Standards Clixon partially supports: +- [RFC5277](http://www.rfc-base.org/txt/rfc-5277.txt) NETCONF Event Notifications +- [RFC6020](https://www.rfc-editor.org/rfc/rfc6020.txt) YANG - A Data Modeling Language for the Network Configuration Protocol (NETCONF) +- [RFC6241](http://www.rfc-base.org/txt/rfc-6241.txt) NETCONF Configuration Protocol +- [RFC6242](http://www.rfc-base.org/txt/rfc-6242.txt) Using the NETCONF Configuration Protocol over Secure Shell (SSH) - [RFC7895](http://www.rfc-base.org/txt/rfc-7895.txt) YANG Module Library * [RFC7950](http://www.rfc-base.org/txt/rfc-7950.txt) The YANG 1.1 Data Modeling Language * [RFC7951](http://www.rfc-base.org/txt/rfc-7951.txt) JSON Encoding of Data Modeled with YANG -- [RFC 6241: NETCONF Configuration Protocol](http://www.rfc-base.org/txt/rfc-6241.txt) -- [RFC 6242: Using the NETCONF Configuration Protocol over Secure Shell (SSH)](http://www.rfc-base.org/txt/rfc-6242.txt) -- [RFC 5277: NETCONF Event Notifications](http://www.rfc-base.org/txt/rfc-5277.txt) -- [RFC 8341: Network Configuration Access Control Model](http://www.rfc-base.org/txt/rfc-8341.txt) -- [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040). -- [RFC8341(NACM)](https://tools.ietf.org/html/rfc8341). +- [RFC8040](https://tools.ietf.org/html/rfc8040) RESTCONF Protocol +- [RFC8341](http://www.rfc-base.org/txt/rfc-8341.txt) Network Configuration Access Control Model - [XML 1.0](https://www.w3.org/TR/2008/REC-xml-20081126) - [Namespaces in XML 1.0](https://www.w3.org/TR/2009/REC-xml-names-20091208) - [XPATH 1.0](https://www.w3.org/TR/xpath-10) diff --git a/apps/backend/Makefile.in b/apps/backend/Makefile.in index 45677854..0a3c0ea3 100644 --- a/apps/backend/Makefile.in +++ b/apps/backend/Makefile.in @@ -61,7 +61,7 @@ CLIXON_LIB = libclixon$(SH_SUFFIX).$(CLIXON_MAJOR).$(CLIXON_MINOR) # even though it may exist in $(libdir). But the new version may not have been installed yet. LIBDEPS = $(top_srcdir)/lib/src/$(CLIXON_LIB) -LIBS = -L$(top_srcdir)/lib/src @LIBS@ $(top_srcdir)/lib/src/$(CLIXON_LIB) -lpthread +LIBS = -L$(top_srcdir)/lib/src @LIBS@ $(top_srcdir)/lib/src/$(CLIXON_LIB) CPPFLAGS = @CPPFLAGS@ -fPIC INCLUDES = -I. -I$(top_srcdir)/lib/src -I$(top_srcdir)/lib -I$(top_srcdir)/include -I$(top_srcdir) @INCLUDES@ diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index 8280ebbe..e8437a88 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -149,6 +149,37 @@ backend_client_rm(clicon_handle h, return backend_client_delete(h, ce); /* actually purge it */ } +/*! + * Maybe should be in the restconf client instead of backend? + * @param[in] h Clicon handle + * @param[in] yspec Yang spec + * @param[in] xpath Xpath selection, not used but may be to filter early + * @param[out] xrs XML restconf-state node + * @see netconf_create_hello + * @see rfc8040 Sections 9.1 + */ +static int +client_get_capabilities(clicon_handle h, + yang_stmt *yspec, + char *xpath, + cxobj **xret) +{ + int retval = -1; + cxobj *xrstate = NULL; /* xml restconf-state node */ + cxobj *xcap = NULL; /* xml capabilities node */ + + if ((xrstate = xpath_first(*xret, "restconf-state")) == NULL){ + clicon_err(OE_YANG, ENOENT, "restconf-state not found in config node"); + goto done; + } + if ((xcap = xml_new("capabilities", xrstate, yspec)) == NULL) + goto done; + if (xml_parse_va(&xcap, yspec, "urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit") < 0) + goto done; + retval = 0; + done: + return retval; +} /*! Get streams state according to RFC 8040 or RFC5277 common function * @param[in] h Clicon handle @@ -165,23 +196,18 @@ static int client_get_streams(clicon_handle h, yang_stmt *yspec, char *xpath, - char *module, + yang_stmt *ymod, char *top, cxobj **xret) { int retval = -1; - yang_stmt *ystream = NULL; /* yang stream module */ yang_stmt *yns = NULL; /* yang namespace */ cxobj *x = NULL; cbuf *cb = NULL; int ret; - if ((ystream = yang_find(yspec, Y_MODULE, module)) == NULL){ - clicon_err(OE_YANG, 0, "%s yang module not found", module); - goto done; - } - if ((yns = yang_find(ystream, Y_NAMESPACE, NULL)) == NULL){ - clicon_err(OE_YANG, 0, "%s yang namespace not found", module); + if ((yns = yang_find(ymod, Y_NAMESPACE, NULL)) == NULL){ + clicon_err(OE_YANG, 0, "%s yang namespace not found", yang_argument_get(ymod)); goto done; } if ((cb = cbuf_new()) == NULL){ @@ -189,6 +215,9 @@ client_get_streams(clicon_handle h, goto done; } cprintf(cb,"<%s xmlns=\"%s\">", top, yang_argument_get(yns)); + /* Second argument is a hack to have the same function for the + * RFC5277 and 8040 stream cases + */ if (stream_get_xml(h, strcmp(top,"restconf-state")==0, cb) < 0) goto done; cprintf(cb,"", top); @@ -234,23 +263,47 @@ client_statedata(clicon_handle h, size_t xlen; int i; yang_stmt *yspec; + yang_stmt *ymod; int ret; + char *namespace; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec"); goto done; } if (clicon_option_bool(h, "CLICON_STREAM_DISCOVERY_RFC5277")){ - if ((ret = client_get_streams(h, yspec, xpath, "clixon-rfc5277", "netconf", xret)) < 0) + if ((ymod = yang_find_module_by_name(yspec, "clixon-rfc5277")) == NULL){ + clicon_err(OE_YANG, ENOENT, "yang module clixon-rfc5277 not found"); + goto done; + } + if ((namespace = yang_find_mynamespace(ymod)) == NULL){ + clicon_err(OE_YANG, ENOENT, "clixon-rfc5277 namespace not found"); + goto done; + } + if (xml_parse_va(xret, yspec, "", namespace) < 0) + goto done; + if ((ret = client_get_streams(h, yspec, xpath, ymod, "netconf", xret)) < 0) goto done; if (ret == 0) goto fail; } if (clicon_option_bool(h, "CLICON_STREAM_DISCOVERY_RFC8040")){ - if ((ret = client_get_streams(h, yspec, xpath, "ietf-restconf-monitoring", "restconf-state", xret)) < 0) + if ((ymod = yang_find_module_by_name(yspec, "ietf-restconf-monitoring")) == NULL){ + clicon_err(OE_YANG, ENOENT, "yang module ietf-restconf-monitoring not found"); + goto done; + } + if ((namespace = yang_find_mynamespace(ymod)) == NULL){ + clicon_err(OE_YANG, ENOENT, "ietf-restconf-monitoring namespace not found"); + goto done; + } + if (xml_parse_va(xret, yspec, "", namespace) < 0) + goto done; + if ((ret = client_get_streams(h, yspec, xpath, ymod, "restconf-state", xret)) < 0) goto done; if (ret == 0) goto fail; + if ((ret = client_get_capabilities(h, yspec, xpath, xret)) < 0) + goto done; } if (clicon_option_bool(h, "CLICON_MODULE_LIBRARY_RFC7895")){ if ((ret = yang_modules_state_get(h, yspec, xpath, nsc, 0, xret)) < 0) diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c index 5e9a5354..26fb145b 100644 --- a/apps/backend/backend_main.c +++ b/apps/backend/backend_main.c @@ -332,6 +332,7 @@ main(int argc, cbuf *cbret = NULL; /* startup cbuf if invalid */ enum startup_status status = STARTUP_ERR; /* Startup status */ int ret; + char *dir; /* In the startup, logs to stderr & syslog and debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, logdst); @@ -573,6 +574,13 @@ main(int argc, if ((yspec = yspec_new()) == NULL) goto done; clicon_dbspec_yang_set(h, yspec); + + /* Load backend plugins before yangs are loaded (eg extension callbacks) */ + if ((dir = clicon_backend_dir(h)) != NULL && + clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, + clicon_option_str(h, "CLICON_BACKEND_REGEXP")) < 0) + goto done; + /* Load Yang modules * 1. Load a yang module as a specific absolute filename */ if ((str = clicon_yang_main_file(h)) != NULL) @@ -596,6 +604,9 @@ main(int argc, /* Add netconf yang spec, used by netconf client and as internal protocol */ if (netconf_module_load(h) < 0) goto done; + /* Load yang restconf module */ + if (yang_spec_parse_module(h, "ietf-restconf", NULL, yspec)< 0) + goto done; /* Load yang Restconf stream discovery */ if (clicon_option_bool(h, "CLICON_STREAM_DISCOVERY_RFC8040") && yang_spec_parse_module(h, "ietf-restconf-monitoring", NULL, yspec)< 0) @@ -646,8 +657,6 @@ main(int argc, goto done; case SM_NONE: /* Fall through * * Load plugins and call plugin_init() */ - if (backend_plugin_initiate(h) != 0) - goto done; status = STARTUP_OK; break; case SM_RUNNING: /* Use running as startup */ @@ -735,7 +744,6 @@ main(int argc, clicon_err(OE_DEMON, errno, "Setting signal"); goto done; } - /* Initialize server socket and save it to handle */ if ((ss = backend_server_socket(h)) < 0) goto done; diff --git a/apps/backend/backend_plugin.c b/apps/backend/backend_plugin.c index 711a8de0..a0dc92d3 100644 --- a/apps/backend/backend_plugin.c +++ b/apps/backend/backend_plugin.c @@ -63,23 +63,6 @@ #include "backend_plugin.h" #include "backend_commit.h" -/*! Load a plugin group. - * @param[in] h Clicon handle - * @retval 0 OK - * @retval -1 Error - */ -int -backend_plugin_initiate(clicon_handle h) -{ - char *dir; - - /* Load application plugins */ - if ((dir = clicon_backend_dir(h)) == NULL) - return 0; - return clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, - clicon_option_str(h, "CLICON_BACKEND_REGEXP")); -} - /*! Request plugins to reset system state * The system 'state' should be the same as the contents of running_db * @param[in] h Clicon handle diff --git a/apps/backend/backend_plugin.h b/apps/backend/backend_plugin.h index 1071fc4e..270272c7 100644 --- a/apps/backend/backend_plugin.h +++ b/apps/backend/backend_plugin.h @@ -67,8 +67,6 @@ typedef struct { /* * Prototypes */ -int backend_plugin_initiate(clicon_handle h); - int clixon_plugin_reset(clicon_handle h, char *db); int clixon_plugin_statedata(clicon_handle h, yang_stmt *yspec, cvec *nsc, diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c index 7de92e44..35204067 100644 --- a/apps/backend/backend_startup.c +++ b/apps/backend/backend_startup.c @@ -138,9 +138,6 @@ startup_mode_startup(clicon_handle h, clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db); goto done; } - /* Load plugins and call plugin_init() */ - if (backend_plugin_initiate(h) != 0) - goto done; /* If startup does not exist, create it empty */ if (xmldb_exists(h, db) != 1){ /* diff */ if (xmldb_create(h, db) < 0) /* diff */ diff --git a/apps/cli/cli_common.c b/apps/cli/cli_common.c index 2bbce242..81ceb677 100644 --- a/apps/cli/cli_common.c +++ b/apps/cli/cli_common.c @@ -47,9 +47,6 @@ #include #include -#ifdef HAVE_CRYPT_H -#include -#endif #include #include #include diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 3ca2c904..4651c5fc 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -284,6 +284,7 @@ main(int argc, char **argv) struct passwd *pw; char *str; int tabmode; + char *dir; /* Defaults */ once = 0; @@ -466,6 +467,11 @@ main(int argc, char **argv) */ cv_exclude_keys(clicon_cli_varonly(h)); + /* Load cli plugins before yangs are loaded (eg extension callbacks) */ + if ((dir = clicon_cli_dir(h)) != NULL && + clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) + goto done; + /* Create top-level and store as option */ if ((yspec = yspec_new()) == NULL) goto done; diff --git a/apps/cli/cli_plugin.c b/apps/cli/cli_plugin.c index 2b2f5791..31331f21 100644 --- a/apps/cli/cli_plugin.c +++ b/apps/cli/cli_plugin.c @@ -319,7 +319,6 @@ int cli_syntax_load(clicon_handle h) { int retval = -1; - char *plugin_dir = NULL; char *clispec_dir = NULL; char *clispec_file = NULL; int ndp; @@ -336,7 +335,6 @@ cli_syntax_load(clicon_handle h) return 0; /* Format plugin directory path */ - plugin_dir = clicon_cli_dir(h); clispec_dir = clicon_clispec_dir(h); clispec_file = clicon_option_str(h, "CLICON_CLISPEC_FILE"); @@ -349,10 +347,6 @@ cli_syntax_load(clicon_handle h) cli_syntax_set(h, stx); - /* Load cli plugins */ - if (plugin_dir && - clixon_plugins_load(h, CLIXON_PLUGIN_INIT, plugin_dir, NULL)< 0) - goto done; if (clispec_file){ if (cli_load_syntax(h, clispec_file, NULL) < 0) goto done; diff --git a/apps/cli/cli_show.c b/apps/cli/cli_show.c index 6cf77547..635bbf8b 100644 --- a/apps/cli/cli_show.c +++ b/apps/cli/cli_show.c @@ -48,9 +48,6 @@ #include #include -#ifdef HAVE_CRYPT_H -#include -#endif #include #include #include diff --git a/apps/netconf/netconf_main.c b/apps/netconf/netconf_main.c index a2c90486..32e81496 100644 --- a/apps/netconf/netconf_main.c +++ b/apps/netconf/netconf_main.c @@ -488,6 +488,12 @@ main(int argc, if ((yspec = yspec_new()) == NULL) goto done; clicon_dbspec_yang_set(h, yspec); + + /* Load netconf plugins before yangs are loaded (eg extension callbacks) */ + if ((dir = clicon_netconf_dir(h)) != NULL && + clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) + goto done; + /* Load Yang modules * 1. Load a yang module as a specific absolute filename */ if ((str = clicon_yang_main_file(h)) != NULL){ @@ -514,15 +520,9 @@ main(int argc, /* Add netconf yang spec, used by netconf client and as internal protocol */ if (netconf_module_load(h) < 0) goto done; - /* Initialize plugins group */ - if ((dir = clicon_netconf_dir(h)) != NULL) - if (clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) - goto done; - /* Call start function is all plugins before we go interactive */ if (clixon_plugin_start(h) < 0) goto done; - if (!quiet) send_hello(h, 1); if (event_reg_fd(0, netconf_input_cb, h, "netconf socket") < 0) diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index e7613f87..05cf76e8 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -464,7 +464,7 @@ api_return_err(clicon_handle h, } else{ FCGX_FPrintF(r->out, "{"); - FCGX_FPrintF(r->out, "\"ietf-restconf:errors\" : "); + FCGX_FPrintF(r->out, "\"ietf-restconf:errors\":"); FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); FCGX_FPrintF(r->out, "}\r\n"); } diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index cc6dabef..e8cbe8a2 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -190,6 +190,7 @@ api_well_known(clicon_handle h, FCGX_Request *r) { clicon_debug(1, "%s", __FUNCTION__); + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Content-Type: application/xrd+xml\r\n"); FCGX_FPrintF(r->out, "\r\n"); FCGX_SetExitStatus(200, r->out); /* OK */ @@ -210,24 +211,32 @@ static int api_root(clicon_handle h, FCGX_Request *r) { - int retval = -1; - char *media_accept; - int use_xml = 0; /* By default use JSON */ - cxobj *xt = NULL; - cbuf *cb = NULL; - int pretty; + int retval = -1; + char *media_accept; + int use_xml = 0; /* By default use JSON */ + cxobj *xt = NULL; + cbuf *cb = NULL; + int pretty; + yang_stmt *yspec; clicon_debug(1, "%s", __FUNCTION__); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp); if (strcmp(media_accept, "application/yang-data+xml")==0) use_xml++; clicon_debug(1, "%s use-xml:%d media-accept:%s", __FUNCTION__, use_xml, media_accept); FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); FCGX_FPrintF(r->out, "\r\n"); - if (xml_parse_string("2016-06-21", NULL, &xt) < 0) + if (xml_parse_string("2016-06-21", NULL, &xt) < 0) + goto done; + if (xml_apply(xt, CX_ELMNT, xml_spec_populate, yspec) < 0) goto done; if ((cb = cbuf_new()) == NULL){ clicon_err(OE_XML, errno, "cbuf_new"); @@ -274,6 +283,7 @@ api_yang_library_version(clicon_handle h, if (strcmp(media_accept, "application/yang-data+xml")==0) use_xml++; FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); FCGX_FPrintF(r->out, "\r\n"); if (xml_parse_va(&xt, NULL, "%s", ietf_yang_library_revision) < 0) @@ -467,6 +477,41 @@ restconf_sig_term(int arg) exit(-1); } +/*! Callback for yang extensions ietf-restconf:yang-data + * @see ietf-restconf.yang + * @param[in] h Clixon handle + * @param[in] yext Yang node of extension + * @param[in] ys Yang node of (unknown) statement belonging to extension + * @retval 0 OK, all callbacks executed OK + * @retval -1 Error in one callback + */ +static int +restconf_main_extension_cb(clicon_handle h, + yang_stmt *yext, + yang_stmt *ys) +{ + int retval = -1; + char *extname; + char *modname; + yang_stmt *ymod; + yang_stmt *yc; + + ymod = ys_module(yext); + modname = yang_argument_get(ymod); + extname = yang_argument_get(yext); + if (strcmp(modname, "ietf-restconf") != 0 || strcmp(extname, "yang-data") != 0) + goto ok; + clicon_debug(1, "%s Enabled extension:%s:%s", __FUNCTION__, modname, extname); + if ((yc = ys_prune(ys, 0)) == NULL) + goto done; + if (yn_insert(yang_parent_get(ys), yc) < 0) + goto done; + ok: + retval = 0; + done: + return retval; +} + static void restconf_sig_child(int arg) { @@ -510,22 +555,23 @@ int main(int argc, char **argv) { - int retval = -1; - int sock; - char *argv0 = argv[0]; - FCGX_Request request; - FCGX_Request *r = &request; - int c; - char *sockpath; - char *path; - clicon_handle h; - char *dir; - int logdst = CLICON_LOG_SYSLOG; - yang_stmt *yspec = NULL; - yang_stmt *yspecfg = NULL; /* For config XXX clixon bug */ - char *stream_path; - int finish; - char *str; + int retval = -1; + int sock; + char *argv0 = argv[0]; + FCGX_Request request; + FCGX_Request *r = &request; + int c; + char *sockpath; + char *path; + clicon_handle h; + char *dir; + int logdst = CLICON_LOG_SYSLOG; + yang_stmt *yspec = NULL; + yang_stmt *yspecfg = NULL; /* For config XXX clixon bug */ + char *stream_path; + int finish; + char *str; + clixon_plugin *cp = NULL; /* In the startup, logs to stderr & debug flag set later */ clicon_log_init(__PROGRAM__, LOG_INFO, logdst); @@ -635,15 +681,21 @@ main(int argc, /* Access the remaining argv/argc options (after --) w clicon-argv_get() */ clicon_argv_set(h, argv0, argc, argv); - /* Initialize plugins group */ - if ((dir = clicon_restconf_dir(h)) != NULL) - if (clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) - return -1; - /* Create top-level yang spec and store as option */ if ((yspec = yspec_new()) == NULL) goto done; - clicon_dbspec_yang_set(h, yspec); + clicon_dbspec_yang_set(h, yspec); + + /* Load restconf plugins before yangs are loaded (eg extension callbacks) */ + if ((dir = clicon_restconf_dir(h)) != NULL) + if (clixon_plugins_load(h, CLIXON_PLUGIN_INIT, dir, NULL) < 0) + return -1; + /* Create a pseudo-plugin to create extension callback to set the ietf-routing + * yang-data extension for api-root top-level restconf function. + */ + if (clixon_pseudo_plugin(h, "pseudo restconf", &cp) < 0) + goto done; + cp->cp_api.ca_extension = restconf_main_extension_cb; /* Load Yang modules * 1. Load a yang module as a specific absolute filename */ @@ -669,6 +721,10 @@ main(int argc, if (yang_modules_init(h) < 0) goto done; + /* Load yang restconf module */ + if (yang_spec_parse_module(h, "ietf-restconf", NULL, yspec)< 0) + goto done; + /* Add netconf yang spec, used as internal protocol */ if (netconf_module_load(h) < 0) goto done; diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index cc7f9f49..4208eca2 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -326,6 +326,7 @@ api_data_get2(clicon_handle h, } clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); FCGX_FPrintF(r->out, "\r\n"); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); @@ -663,7 +664,6 @@ api_data_post(clicon_handle h, clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } - FCGX_SetExitStatus(201, r->out); /* Created */ FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); diff --git a/configure b/configure index d1172d87..2a1df8d0 100755 --- a/configure +++ b/configure @@ -2172,9 +2172,9 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu : ${INSTALLFLAGS="-s"} CLIXON_VERSION_MAJOR="4" -CLIXON_VERSION_MINOR="0" +CLIXON_VERSION_MINOR="1" CLIXON_VERSION_PATCH="0" -CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}\"" +CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}.PRE\"" # Check CLIgen if test "$prefix" = "NONE"; then @@ -3645,45 +3645,6 @@ if test "$prefix" = "NONE"; then prefix=${ac_default_prefix} fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for main in -lm" >&5 -$as_echo_n "checking for main in -lm... " >&6; } -if ${ac_cv_lib_m_main+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS -LIBS="-lm $LIBS" -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - - -int -main () -{ -return main (); - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_m_main=yes -else - ac_cv_lib_m_main=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_m_main" >&5 -$as_echo "$ac_cv_lib_m_main" >&6; } -if test "x$ac_cv_lib_m_main" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBM 1 -_ACEOF - - LIBS="-lm $LIBS" - -fi - SH_SUFFIX=".so" # This is for cligen @@ -4195,63 +4156,6 @@ if test "${with_configfile+set}" = set; then : fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for crypt in -lcrypt" >&5 -$as_echo_n "checking for crypt in -lcrypt... " >&6; } -if ${ac_cv_lib_crypt_crypt+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS -LIBS="-lcrypt $LIBS" -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -/* Override any GCC internal prototype to avoid an error. - Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -#ifdef __cplusplus -extern "C" -#endif -char crypt (); -int -main () -{ -return crypt (); - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_crypt_crypt=yes -else - ac_cv_lib_crypt_crypt=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_crypt_crypt" >&5 -$as_echo "$ac_cv_lib_crypt_crypt" >&6; } -if test "x$ac_cv_lib_crypt_crypt" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBCRYPT 1 -_ACEOF - - LIBS="-lcrypt $LIBS" - -fi - -for ac_header in crypt.h -do : - ac_fn_c_check_header_mongrel "$LINENO" "crypt.h" "ac_cv_header_crypt_h" "$ac_includes_default" -if test "x$ac_cv_header_crypt_h" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_CRYPT_H 1 -_ACEOF - -fi - -done - # user credentials for unix sockets for ac_header in sys/ucred.h @@ -4269,20 +4173,6 @@ fi done -# This is for Linux vlan code -for ac_header in linux/if_vlan.h -do : - ac_fn_c_check_header_mongrel "$LINENO" "linux/if_vlan.h" "ac_cv_header_linux_if_vlan_h" "$ac_includes_default" -if test "x$ac_cv_header_linux_if_vlan_h" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LINUX_IF_VLAN_H 1 -_ACEOF - -fi - -done - - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for socket in -lsocket" >&5 $as_echo_n "checking for socket in -lsocket... " >&6; } if ${ac_cv_lib_socket_socket+:} false; then : @@ -4328,51 +4218,6 @@ _ACEOF fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for xdr_char in -lnsl" >&5 -$as_echo_n "checking for xdr_char in -lnsl... " >&6; } -if ${ac_cv_lib_nsl_xdr_char+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_check_lib_save_LIBS=$LIBS -LIBS="-lnsl $LIBS" -cat confdefs.h - <<_ACEOF >conftest.$ac_ext -/* end confdefs.h. */ - -/* Override any GCC internal prototype to avoid an error. - Use char because int might match the return type of a GCC - builtin and then its argument prototype would still apply. */ -#ifdef __cplusplus -extern "C" -#endif -char xdr_char (); -int -main () -{ -return xdr_char (); - ; - return 0; -} -_ACEOF -if ac_fn_c_try_link "$LINENO"; then : - ac_cv_lib_nsl_xdr_char=yes -else - ac_cv_lib_nsl_xdr_char=no -fi -rm -f core conftest.err conftest.$ac_objext \ - conftest$ac_exeext conftest.$ac_ext -LIBS=$ac_check_lib_save_LIBS -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_nsl_xdr_char" >&5 -$as_echo "$ac_cv_lib_nsl_xdr_char" >&6; } -if test "x$ac_cv_lib_nsl_xdr_char" = xyes; then : - cat >>confdefs.h <<_ACEOF -#define HAVE_LIBNSL 1 -_ACEOF - - LIBS="-lnsl $LIBS" - -fi - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for dlopen in -ldl" >&5 $as_echo_n "checking for dlopen in -ldl... " >&6; } if ${ac_cv_lib_dl_dlopen+:} false; then : diff --git a/configure.ac b/configure.ac index ce48aff5..59c9658b 100644 --- a/configure.ac +++ b/configure.ac @@ -43,9 +43,9 @@ AC_INIT(lib/clixon/clixon.h.in) : ${INSTALLFLAGS="-s"} CLIXON_VERSION_MAJOR="4" -CLIXON_VERSION_MINOR="0" +CLIXON_VERSION_MINOR="1" CLIXON_VERSION_PATCH="0" -CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}\"" +CLIXON_VERSION="\"${CLIXON_VERSION_MAJOR}.${CLIXON_VERSION_MINOR}.${CLIXON_VERSION_PATCH}.PRE\"" # Check CLIgen if test "$prefix" = "NONE"; then @@ -131,7 +131,6 @@ if test "$prefix" = "NONE"; then prefix=${ac_default_prefix} fi -AC_CHECK_LIB(m, main) SH_SUFFIX=".so" # This is for cligen @@ -202,19 +201,13 @@ AC_ARG_WITH([configfile], [AS_HELP_STRING([--with-configfile=FILE],[set default path to config file])], [CLIXON_DEFAULT_CONFIG="$withval"],) -AC_CHECK_LIB(crypt, crypt) -AC_CHECK_HEADERS(crypt.h) # user credentials for unix sockets AC_CHECK_HEADERS([sys/ucred.h],[],[], [[# include ]] ) -# This is for Linux vlan code -AC_CHECK_HEADERS(linux/if_vlan.h) - AC_CHECK_LIB(socket, socket) -AC_CHECK_LIB(nsl, xdr_char) AC_CHECK_LIB(dl, dlopen) # This is for libxml2 XSD regex engine diff --git a/doc/FAQ.md b/doc/FAQ.md index de9f60d0..4c250cdf 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -61,6 +61,7 @@ General Public License Version 2. ## Is Clixon extendible? Yes. All application semantics is defined in plugins with well-defined APIs. There are currently plugins for: CLI, Netconf, Restconf, the datastore and the backend. +Clixon also supports Yang extensions, see main example. ## Which programming language is used? Clixon is written in C. The plugins are written in C. The CLI diff --git a/example/main/Makefile.in b/example/main/Makefile.in index 363d460b..36a27dc0 100644 --- a/example/main/Makefile.in +++ b/example/main/Makefile.in @@ -79,7 +79,7 @@ all: $(PLUGINS) CLISPECS = $(APPNAME)_cli.cli -YANGSPECS = clixon-example@2019-01-13.yang +YANGSPECS = clixon-example@2019-07-23.yang # Backend plugin BE_SRC = $(APPNAME)_backend.c diff --git a/example/main/README.md b/example/main/README.md index 2d00dec2..441e5aee 100644 --- a/example/main/README.md +++ b/example/main/README.md @@ -7,6 +7,7 @@ * [Streams](#streams) * [RPC Operations](#rpc-operations) * [State data](#state-data) + * [Extensions](#extension) * [Authentication and NACM](#authentication-and-nacm) * [Systemd](#systemd) * [Docker](#docker) @@ -270,6 +271,33 @@ The example contains some stubs for authorization according to [RFC8341(NACM)](h * A basic auth HTTP callback, see: example_restconf_credentials() containing three example users: andy, wilma, and guest, according to the examples in Appendix A in [RFC8341](https://tools.ietf.org/html/rfc8341). * A NACM backend plugin reporting the mandatory NACM state variables. +## Extensions + +Clixon supports Yang extensions, but you need to write plugin code. +The example backend implements an "example:e4" Yang extension, as follows: +``` + extension e4 { + description + "The first child of the ex:e4 (unknown) statement is replaced with its first + child. This means that 'uses bar;' in the ex:e4 statement below is a valid + data node"; + argument arg; + } + ex:e4 arg1{ + uses bar; + } +``` + +The backend plugin code registers an extension callback in the init struct: +``` + .ca_extension=example_extension, /* yang extensions */ +``` + +The callback then receives a callback on all "unknown" Yang statements +during yang parsing. If the extension matches "example:e4", it applies +the extension. In the example, it replaces the "ex:e4" statements with +its first child, making it a proper yang statement. + ## Systemd Example systemd files for backend and restconf daemons are found under the systemd directory. Install them under /etc/systemd/system for example. diff --git a/example/main/clixon-example@2019-01-13.yang b/example/main/clixon-example@2019-07-23.yang similarity index 85% rename from example/main/clixon-example@2019-01-13.yang rename to example/main/clixon-example@2019-07-23.yang index da038f31..cfb4c629 100644 --- a/example/main/clixon-example@2019-01-13.yang +++ b/example/main/clixon-example@2019-07-23.yang @@ -2,9 +2,11 @@ module clixon-example { yang-version 1.1; namespace "urn:example:clixon"; prefix ex; + revision 2019-07-23 { + description "Extension e4. Released in Clixon 4.1.0"; + } revision 2019-01-13 { - description - "Released in Clixon 3.9"; + description "Released in Clixon 3.9"; } import ietf-interfaces { prefix if; @@ -40,6 +42,23 @@ module clixon-example { type string; } } + /* yang extension implemented by the example backend code. */ + extension e4 { + description + "The first child of the ex:e4 (unknown) statement is replaced with its first + child. This means that 'uses bar;' in the ex:e4 statement below is a valid + data node"; + argument arg; + } + grouping bar { + leaf bar{ + type string; + } + } + ex:e4 arg1{ + uses bar; + } + /* Example notification as used in RFC 5277 and RFC 8040 */ notification event { description "Example notification event."; diff --git a/example/main/example_backend.c b/example/main/example_backend.c index 6c2040a9..5a4f3b29 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -351,6 +351,42 @@ example_statedata(clicon_handle h, return retval; } +/*! Callback for yang extensions example:e4 + * + * @param[in] h Clixon handle + * @param[in] yext Yang node of extension + * @param[in] ys Yang node of (unknown) statement belonging to extension + * @retval 0 OK, all callbacks executed OK + * @retval -1 Error in one callback + */ +int +example_extension(clicon_handle h, + yang_stmt *yext, + yang_stmt *ys) +{ + int retval = -1; + char *extname; + char *modname; + yang_stmt *ymod; + yang_stmt *yc; + + ymod = ys_module(yext); + modname = yang_argument_get(ymod); + extname = yang_argument_get(yext); + if (strcmp(modname, "example") != 0 || strcmp(extname, "e4") != 0) + goto ok; + clicon_debug(1, "%s Enabled extension:%s:%s", __FUNCTION__, modname, extname); + if ((yc = ys_prune(ys, 0)) == NULL) + goto done; + if (yn_insert(yang_parent_get(ys), yc) < 0) + goto done; + ok: + retval = 0; + done: + return retval; +} + + /*! Testcase upgrade function moving interfaces-state to interfaces * @param[in] h Clicon handle * @param[in] xn XML tree to be updated @@ -616,6 +652,7 @@ static clixon_plugin_api api = { clixon_plugin_init, /* init - must be called clixon_plugin_init */ example_start, /* start */ example_exit, /* exit */ + .ca_extension=example_extension, /* yang extensions */ .ca_reset=example_reset, /* reset */ .ca_statedata=example_statedata, /* statedata */ .ca_trans_begin=main_begin, /* trans begin */ diff --git a/include/clixon_config.h.in b/include/clixon_config.h.in index 7bd27d76..a5b70be8 100644 --- a/include/clixon_config.h.in +++ b/include/clixon_config.h.in @@ -24,9 +24,6 @@ /* Define to 1 if you have the header file. */ #undef HAVE_CLIGEN_CLIGEN_H -/* Define to 1 if you have the header file. */ -#undef HAVE_CRYPT_H - /* Define to 1 if you have the `inet_aton' function. */ #undef HAVE_INET_ATON @@ -36,9 +33,6 @@ /* Define to 1 if you have the `cligen' library (-lcligen). */ #undef HAVE_LIBCLIGEN -/* Define to 1 if you have the `crypt' library (-lcrypt). */ -#undef HAVE_LIBCRYPT - /* Define to 1 if you have the `curl' library (-lcurl). */ #undef HAVE_LIBCURL @@ -48,21 +42,12 @@ /* Define to 1 if you have the `fcgi' library (-lfcgi). */ #undef HAVE_LIBFCGI -/* Define to 1 if you have the `m' library (-lm). */ -#undef HAVE_LIBM - -/* Define to 1 if you have the `nsl' library (-lnsl). */ -#undef HAVE_LIBNSL - /* Define to 1 if you have the `socket' library (-lsocket). */ #undef HAVE_LIBSOCKET /* Define to 1 if you have the `xml2' library (-lxml2). */ #undef HAVE_LIBXML2 -/* Define to 1 if you have the header file. */ -#undef HAVE_LINUX_IF_VLAN_H - /* Define to 1 if you have the header file. */ #undef HAVE_MEMORY_H diff --git a/lib/clixon/clixon_json.h b/lib/clixon/clixon_json.h index d4b2b3d4..30488d0b 100644 --- a/lib/clixon/clixon_json.h +++ b/lib/clixon/clixon_json.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -32,6 +32,8 @@ ***** END LICENSE BLOCK ***** * JSON support functions. + * JSON syntax is according to: + * http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf */ #ifndef _CLIXON_JSON_H #define _CLIXON_JSON_H diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index ce8e20e1..2c91e0b9 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -106,6 +106,20 @@ typedef int (plgstart_t)(clicon_handle); /* Plugin start */ */ typedef int (plgexit_t)(clicon_handle); /* Plugin exit */ +/* For yang extension handling. + * Called at parsing of yang module containing a statement of an extension. + * A plugin may identify the extension by its name, and perform actions + * on the yang statement, such as transforming the yang. + * A callback is made for every statement, which means that several calls per + * extension can be made. + * @param[in] h Clixon handle + * @param[in] yext Yang node of extension + * @param[in] ys Yang node of (unknown) statement belonging to extension + * @retval 0 OK, all callbacks executed OK + * @retval -1 Error in one callback + */ +typedef int (plgextension_t)(clicon_handle h, yang_stmt *yext, yang_stmt *ys); + /*! Called by restconf to check credentials and return username */ @@ -168,6 +182,7 @@ struct clixon_plugin_api{ plginit2_t *ca_init; /* Clixon plugin Init (implicit) */ plgstart_t *ca_start; /* Plugin start */ plgexit_t *ca_exit; /* Plugin exit */ + plgextension_t *ca_extension; /* Yang extension handler */ union { struct { /* cli-specific */ cli_prompthook_t *ci_prompt; /* Prompt hook */ @@ -189,7 +204,6 @@ struct clixon_plugin_api{ trans_cb_t *cb_trans_revert; /* Transaction revert */ trans_cb_t *cb_trans_end; /* Transaction completed */ trans_cb_t *cb_trans_abort; /* Transaction aborted */ - } cau_backend; } u; @@ -245,12 +259,16 @@ clixon_plugin *clixon_plugin_find(clicon_handle h, char *name); int clixon_plugins_load(clicon_handle h, char *function, char *dir, char *regexp); +int clixon_pseudo_plugin(clicon_handle h, char *name, clixon_plugin **cpp); + int clixon_plugin_start(clicon_handle h); int clixon_plugin_exit(clicon_handle h); int clixon_plugin_auth(clicon_handle h, void *arg); +int clixon_plugin_extension(clicon_handle h, yang_stmt *yext, yang_stmt *ys); + /* rpc callback API */ int rpc_callback_register(clicon_handle h, clicon_rpc_cb cb, void *arg, char *namespace, char *name); int rpc_callback_delete_all(clicon_handle h); diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index c703f4f0..4070eac2 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -140,7 +140,6 @@ struct xml; typedef struct yang_stmt yang_stmt; /* Defined in clixon_yang_internal */ - typedef int (yang_applyfn_t)(yang_stmt *ys, void *arg); /* @@ -157,6 +156,8 @@ int yang_cvec_set(yang_stmt *ys, cvec *cvv); /* Other functions */ yang_stmt *yspec_new(void); yang_stmt *ys_new(enum rfc_6020 keyw); +yang_stmt *ys_prune(yang_stmt *yp, int i); + int ys_free(yang_stmt *ys); int yspec_free(yang_stmt *yspec); int ys_cp(yang_stmt *new, yang_stmt *old); diff --git a/lib/src/clixon_json.c b/lib/src/clixon_json.c index 6049df0b..609e7e07 100644 --- a/lib/src/clixon_json.c +++ b/lib/src/clixon_json.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -91,14 +91,17 @@ enum array_element_type{ }; enum childtype{ - NULL_CHILD=0, /* eg no children */ + NULL_CHILD=0, /* eg no children. Translated to null if in + * array or leaf terminal, and to {} if proper object, ie container. + * anyxml/anydata? + */ BODY_CHILD, /* eg one child which is a body, eg 1 */ ANY_CHILD, /* eg or */ }; /*! x is element and has exactly one child which in turn has none * remove attributes from x - * Clone from clixon_xml_map.c + * @see tleaf in clixon_xml_map.c */ static enum childtype child_type(cxobj *x) @@ -372,6 +375,7 @@ xml2json1_cbuf(cbuf *cb, modname0 = modname; /* modname0 is ancestor ns passed to child */ } childt = child_type(x); + if (pretty==2) cprintf(cb, "#%s_array, %s_child ", arraytype2str(arraytype), @@ -393,11 +397,17 @@ xml2json1_cbuf(cbuf *cb, cprintf(cb, "%*s\"", pretty?(level*JSON_INDENT):0, ""); if (modname) cprintf(cb, "%s:", modname); - cprintf(cb, "%s\": ", xml_name(x)); + cprintf(cb, "%s\":%s", xml_name(x), pretty?" ":""); } switch (childt){ case NULL_CHILD: - cprintf(cb, "null"); + /* If x is a container, use {} instead of null + * That is, x is not a list or leaf + */ + if (ys && yang_keyword_get(ys) == Y_CONTAINER) + cprintf(cb, "{}"); + else + cprintf(cb, "null"); break; case BODY_CHILD: break; @@ -413,7 +423,7 @@ xml2json1_cbuf(cbuf *cb, cprintf(cb, "%*s\"", pretty?(level*JSON_INDENT):0, ""); if (modname) cprintf(cb, "%s:", modname); - cprintf(cb, "%s\": ", xml_name(x)); + cprintf(cb, "%s\":%s", xml_name(x), pretty?" ":""); level++; cprintf(cb, "[%s%*s", pretty?"\n":"", @@ -443,7 +453,7 @@ xml2json1_cbuf(cbuf *cb, case BODY_CHILD: break; case ANY_CHILD: - cprintf(cb, "{ %s", pretty?"\n":""); + cprintf(cb, "{%s", pretty?"\n":""); break; default: break; diff --git a/lib/src/clixon_plugin.c b/lib/src/clixon_plugin.c index 0f2fd622..c2d5cd3c 100644 --- a/lib/src/clixon_plugin.c +++ b/lib/src/clixon_plugin.c @@ -169,14 +169,14 @@ clixon_plugin_find(clicon_handle h, } /*! Load a dynamic plugin object and call its init-function - * @param[in] h Clicon handle - * @param[in] file Which plugin to load + * @param[in] h Clicon handle + * @param[in] file Which plugin to load * @param[in] function Which function symbol to load and call - * @param[in] dlflags See man(3) dlopen + * @param[in] dlflags See man(3) dlopen * @param[out] cpp Clixon plugin structure (if retval is 1) - * @retval 1 OK - * @retval 0 Failed load, log, skip and continue with other plugins - * @retval -1 Error + * @retval 1 OK + * @retval 0 Failed load, log, skip and continue with other plugins + * @retval -1 Error * @see clixon_plugins_load Load all plugins */ static int @@ -236,8 +236,6 @@ plugin_load_one(clicon_handle h, if ((p=strrchr(name, '.')) != NULL) *p = '\0'; /* Copy name to struct */ - memcpy(cp->cp_name, name, strlen(name)+1); - snprintf(cp->cp_name, sizeof(cp->cp_name), "%*s", (int)strlen(name), name); cp->cp_api = *api; @@ -274,7 +272,7 @@ clixon_plugins_load(clicon_handle h, struct dirent *dp = NULL; int i; char filename[MAXPATHLEN]; - clixon_plugin *cp; + clixon_plugin *cp = NULL; int ret; clicon_debug(1, "%s", __FUNCTION__); @@ -305,6 +303,47 @@ done: return retval; } +/*! Create a pseudo plugin so that a main function can register callbacks + * @param[in] h Clicon handle + * @param[in] name Plugin name + * @param[out] cpp Clixon plugin structure (direct pointer) + * @retval 0 OK, with cpp set + * @retval -1 Error + */ +int +clixon_pseudo_plugin(clicon_handle h, + char *name, + clixon_plugin **cpp) +{ + int retval = -1; + clixon_plugin *cp = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + + /* Create a pseudo plugins */ + /* Note: sizeof clixon_plugin_api which is largest of clixon_plugin_api:s */ + if ((cp = (clixon_plugin *)malloc(sizeof(struct clixon_plugin))) == NULL){ + clicon_err(OE_UNIX, errno, "malloc"); + goto done; + } + memset(cp, 0, sizeof(struct clixon_plugin)); + snprintf(cp->cp_name, sizeof(cp->cp_name), "%*s", (int)strlen(name), name); + + _clixon_nplugins++; + if ((_clixon_plugins = realloc(_clixon_plugins, _clixon_nplugins*sizeof(clixon_plugin))) == NULL) { + clicon_err(OE_UNIX, errno, "realloc"); + goto done; + } + _clixon_plugins[_clixon_nplugins-1] = *cp; + *cpp = &_clixon_plugins[_clixon_nplugins-1]; + + retval = 0; +done: + if (cp) + free(cp); + return retval; +} + /*! Call plugin_start in all plugins * @param[in] h Clicon handle * Call plugin start functions (if defined) @@ -395,11 +434,45 @@ clixon_plugin_auth(clicon_handle h, return retval; } +/*! Callback for a yang extension (unknown) statement + * Called at parsing of yang module containing a statement of an extension. + * A plugin may identify the extension and perform actions + * on the yang statement, such as transforming the yang. + * A callback is made for every statement, which means that several calls per + * extension can be made. + * @param[in] h Clixon handle + * @param[in] yext Yang node of extension + * @param[in] ys Yang node of (unknown) statement belonging to extension + * @retval 0 OK, all callbacks executed OK + * @retval -1 Error in one callback + */ +int +clixon_plugin_extension(clicon_handle h, + yang_stmt *yext, + yang_stmt *ys) +{ + clixon_plugin *cp; + int i; + plgextension_t *extfn; /* Plugin extension fn */ + int retval = 1; + + for (i = 0; i < _clixon_nplugins; i++) { + cp = &_clixon_plugins[i]; + if ((extfn = cp->cp_api.ca_extension) == NULL) + continue; + if ((retval = extfn(h, yext, ys)) < 0) { + clicon_debug(1, "plugin_extension() failed"); + return -1; + } + } + return retval; +} + /*-------------------------------------------------------------------- * RPC callbacks for both client/frontend and backend plugins. * RPC callbacks are explicitly registered in the plugin_init() function * with a tag and a function - * WHen the the tag is encountered, the callback is called. + * When the the tag is encountered, the callback is called. * Primarily backend, but also netconf and restconf frontend plugins. * CLI frontend so far have direct callbacks, ie functions in the cligen * specification are directly dlsym:ed to the CLI plugin. @@ -511,11 +584,11 @@ rpc_callback_call(clicon_handle h, if (rpc_cb_list == NULL) return 0; + name = xml_name(xe); + prefix = xml_prefix(xe); + xml2ns(xe, prefix, &namespace); rc = rpc_cb_list; do { - name = xml_name(xe); - prefix = xml_prefix(xe); - xml2ns(xe, prefix, &namespace); if (strcmp(rc->rc_name, name) == 0 && namespace && rc->rc_namespace && strcmp(rc->rc_namespace, namespace) == 0){ diff --git a/lib/src/clixon_string.c b/lib/src/clixon_string.c index 014ce52b..cfd07b0e 100644 --- a/lib/src/clixon_string.c +++ b/lib/src/clixon_string.c @@ -65,7 +65,7 @@ * if ((vec = clicon_strsep("/home/user/src/clixon", "/", &nvec)) == NULL) * err; * for (i=0; iys_argument) free(ys->ys_argument); - if (ys->ys_extra) - free(ys->ys_extra); if (ys->ys_cv) cv_free(ys->ys_cv); if (ys->ys_cvec) @@ -309,7 +307,7 @@ ys_free1(yang_stmt *ys) * @see ys_free Deallocate yang node * @note Do not call this in a loop of yang children (unless you know what you are doing) */ -static yang_stmt * +yang_stmt * ys_prune(yang_stmt *yp, int i) { @@ -323,7 +321,7 @@ ys_prune(yang_stmt *yp, memmove(&yp->ys_stmt[i], &yp->ys_stmt[i+1], size); - yc = yp->ys_stmt[yp->ys_len--] = NULL;; + yp->ys_stmt[yp->ys_len--] = NULL;; done: return yc; } @@ -411,11 +409,6 @@ ys_cp(yang_stmt *ynew, clicon_err(OE_YANG, errno, "strdup"); goto done; } - if (yold->ys_extra) - if ((ynew->ys_extra = strdup(yold->ys_extra)) == NULL){ - clicon_err(OE_YANG, errno, "strdup"); - goto done; - } if (yold->ys_cv) if ((ynew->ys_cv = cv_dup(yold->ys_cv)) == NULL){ clicon_err(OE_YANG, errno, "cv_dup"); @@ -473,6 +466,8 @@ ys_dup(yang_stmt *old) * * @param[in] ys_parent Add child to this parent * @param[in] ys_child Add this child + * @retval 0 OK + * @retval -1 Error * Also add parent to child as up-pointer */ int @@ -646,7 +641,7 @@ yang_find_datanode(yang_stmt *yn, goto match; } } /* Y_CHOICE */ - else + else{ if (yang_datanode(ys)){ if (argument == NULL) ysmatch = ys; @@ -656,6 +651,7 @@ yang_find_datanode(yang_stmt *yn, if (ysmatch) goto match; } + } } /* Special case: if not match and yang node is module or submodule, extend * search to include submodules */ @@ -1794,44 +1790,46 @@ ys_populate_unique(clicon_handle h, /*! Populate unknown node with extension * @param[in] h Clicon handle * @param[in] ys The yang statement (unknown) to populate. + * RFC 7950 Sec 7.19: + * If no "argument" statement is present, the keyword expects no argument when + * it is used. */ static int ys_populate_unknown(clicon_handle h, yang_stmt *ys) { - int retval = -1; - int cvret; - char *reason = NULL; + int retval = -1; yang_stmt *ymod; + yang_stmt *yext; /* extension */ char *prefix = NULL; char *id = NULL; - char *extra; + char *argument; /* This is the unknown optional argument */ + cg_var *cv; - if ((extra = ys->ys_extra) == NULL) - goto ok; /* Find extension, if found, store it as unknown, if not, break for error */ if (nodeid_split(yang_argument_get(ys), &prefix, &id) < 0) goto done; - if ((ymod = yang_find_module_by_prefix(ys, prefix)) == NULL) - goto ok; /* shouldnt happen */ - if (yang_find(ymod, Y_EXTENSION, id) == NULL){ - clicon_err(OE_YANG, errno, "Extension %s:%s not found", prefix, id); + if ((ymod = yang_find_module_by_prefix(ys, prefix)) == NULL){ + clicon_err(OE_YANG, ENOENT, "Extension %s:%s, module not found", prefix, id); goto done; } - if ((ys->ys_cv = cv_new(CGV_STRING)) == NULL){ - clicon_err(OE_YANG, errno, "cv_new"); + if ((yext = yang_find(ymod, Y_EXTENSION, id)) == NULL){ + clicon_err(OE_YANG, ENOENT, "Extension %s:%s not found", prefix, id); goto done; } - if ((cvret = cv_parse1(extra, ys->ys_cv, &reason)) < 0){ /* error */ - clicon_err(OE_YANG, errno, "parsing cv"); - goto done; + /* Optional argument (only if "argument") - save it in ys_cv */ + if ((cv = yang_cv_get(ys)) != NULL && + (argument = cv_string_get(cv)) != NULL){ + if (yang_find(yext, Y_ARGUMENT, NULL) == NULL && + argument != NULL){ + clicon_err(OE_YANG, 0, "No argument specified in extension %s, but argument %s present when used", yang_argument_get(ys), argument); + goto done; + } } - if (cvret == 0){ /* parsing failed */ - clicon_err(OE_YANG, errno, "Parsing CV: %s", reason); + /* Make extension callbacks that may alter yang structure */ + if (clixon_plugin_extension(h, yext, ys) < 0) goto done; - } - ok: retval = 0; done: if (prefix) @@ -2581,39 +2579,62 @@ yang_parse_recurse(clicon_handle h, return retval; /* top-level (sub)module */ } -int +static int ys_schemanode_check(yang_stmt *ys, - void *arg) + void *dummy) { - int retval = -1; - yang_stmt *yspec; - yang_stmt *yres; - yang_stmt *yp; + int retval = -1; + yang_stmt *yspec; + yang_stmt *yres = NULL; + yang_stmt *yp; + char *arg; + enum rfc_6020 keyword; - yp = ys->ys_parent; - switch (ys->ys_keyword){ + yp = yang_parent_get(ys); + arg = yang_argument_get(ys); + keyword = yang_keyword_get(ys); + switch (yang_keyword_get(ys)){ case Y_AUGMENT: - if (yp->ys_keyword == Y_MODULE || /* Not top-level */ - yp->ys_keyword == Y_SUBMODULE) + if (yang_keyword_get(yp) == Y_MODULE || /* Not top-level */ + yang_keyword_get(yp) == Y_SUBMODULE) break; /* fallthru */ case Y_REFINE: - case Y_UNIQUE: - if (yang_desc_schema_nodeid(yp, ys->ys_argument, -1, &yres) < 0) + if (yang_desc_schema_nodeid(yp, arg, -1, &yres) < 0) goto done; if (yres == NULL){ clicon_err(OE_YANG, 0, "schemanode sanity check of %s %s", - yang_key2str(ys->ys_keyword), - ys->ys_argument); + yang_key2str(keyword), arg); goto done; } break; + case Y_UNIQUE:{ + char **vec = NULL; + char *v; + int nvec; + int i; + /* Unique: Sec 7.8.3 It takes as an argument a string that contains a space- + separated list of schema node identifiers */ + if ((vec = clicon_strsep(arg, " \t\n", &nvec)) == NULL) + goto done; + for (i=0; iys_argument, -1, &yres) < 0) + if (yang_abs_schema_nodeid(yspec, ys, arg, -1, &yres) < 0) goto done; if (yres == NULL){ - clicon_err(OE_YANG, 0, "schemanode sanity check of %s", ys->ys_argument); + clicon_err(OE_YANG, 0, "schemanode sanity check of %s", arg); goto done; } break; @@ -3104,11 +3125,11 @@ yang_datanode(yang_stmt *ys) * @retval 0 OK */ static int -schema_nodeid_vec(yang_stmt *yn, - char **vec, - int nvec, +schema_nodeid_vec(yang_stmt *yn, + char **vec, + int nvec, enum rfc_6020 keyword, - yang_stmt **yres) + yang_stmt **yres) { int retval = -1; char *arg; @@ -3205,6 +3226,7 @@ yang_abs_schema_nodeid(yang_stmt *yspec, char *prefix = NULL; yang_stmt *yprefix; + *yres = NULL; /* check absolute schema_nodeid */ if (schema_nodeid[0] != '/'){ clicon_err(OE_YANG, EINVAL, "absolute schema nodeid should start with /"); @@ -3265,15 +3287,16 @@ yang_abs_schema_nodeid(yang_stmt *yspec, * Used in yang: unique, refine, uses augment */ int -yang_desc_schema_nodeid(yang_stmt *yn, - char *schema_nodeid, +yang_desc_schema_nodeid(yang_stmt *yn, + char *schema_nodeid, enum rfc_6020 keyword, - yang_stmt **yres) + yang_stmt **yres) { int retval = -1; char **vec = NULL; int nvec; + *yres = NULL; if (strlen(schema_nodeid) == 0) goto done; /* check absolute schema_nodeid */ @@ -3454,12 +3477,23 @@ ys_parse_sub(yang_stmt *ys, goto done; } break; - case Y_UNKNOWN: /* XXX This code assumes ymod already loaded - but it may not be */ + case Y_UNKNOWN:{ /* save (optional) argument in ys_cv */ if (extra == NULL) break; - ys->ys_extra = extra; + if ((ys->ys_cv = cv_new(CGV_STRING)) == NULL){ + clicon_err(OE_YANG, errno, "cv_new"); + goto done; + } + if ((ret = cv_parse1(extra, ys->ys_cv, &reason)) < 0){ /* error */ + clicon_err(OE_YANG, errno, "parsing cv"); + goto done; + } + if (ret == 0){ /* parsing failed */ + clicon_err(OE_YANG, errno, "Parsing CV: %s", reason); + goto done; + } break; + } default: break; } @@ -3629,3 +3663,4 @@ yang_key_match(yang_stmt *yn, cvec_free(cvv); return retval; } + diff --git a/lib/src/clixon_yang_internal.h b/lib/src/clixon_yang_internal.h index fe7b2c49..a1410dda 100644 --- a/lib/src/clixon_yang_internal.h +++ b/lib/src/clixon_yang_internal.h @@ -82,8 +82,6 @@ struct yang_stmt{ yang_stmt *ys_mymodule; /* Shortcut to "my" module. Augmented nodes can belong to other modules than the ancestor module */ - - char *ys_extra; /* For unknown */ cg_var *ys_cv; /* cligen variable. See ys_populate() Following stmts have cv:s: leaf: for default value @@ -91,7 +89,7 @@ struct yang_stmt{ config: boolean true or false mandatory: boolean true or false fraction-digits for fraction-digits - unknown-stmt (argument) + unknown-stmt (optional argument) */ cvec *ys_cvec; /* List of stmt-specific variables Y_RANGE: range_min, range_max diff --git a/lib/src/clixon_yang_parse.l b/lib/src/clixon_yang_parse.l index 8eb87f70..1b7c9f46 100644 --- a/lib/src/clixon_yang_parse.l +++ b/lib/src/clixon_yang_parse.l @@ -104,7 +104,7 @@ identifier [A-Za-z_][A-Za-z0-9_\-\.]* %% /* Common tokens */ -[ \t] +[ \t] <> { return MY_EOF; } \n { _YY->yy_linenum++; } \r @@ -199,7 +199,8 @@ identifier [A-Za-z_][A-Za-z0-9_\-\.]* ; { BEGIN(KEYWORD); return *yytext; } \" { _YY->yy_lex_string_state =UNKNOWN; BEGIN(STRINGDQ); return *yytext; } \{ { BEGIN(KEYWORD); return *yytext; } -. { clixon_yang_parselval.string = strdup(yytext); +[ \t]+ { return SEP; } +[^{";: \t]+ { clixon_yang_parselval.string = strdup(yytext); return CHARS; } true { clixon_yang_parselval.string = strdup(yytext); diff --git a/lib/src/clixon_yang_parse.y b/lib/src/clixon_yang_parse.y index 7e9d910a..84c48830 100644 --- a/lib/src/clixon_yang_parse.y +++ b/lib/src/clixon_yang_parse.y @@ -57,6 +57,7 @@ %token MY_EOF %token SQ /* Single quote: ' */ +%token SEP /* Separators (at least one) */ %token CHARS %token IDENTIFIER %token BOOL @@ -1540,26 +1541,33 @@ deviate_substmt : type_stmt { clicon_debug(2,"deviate-substmt -> type-st ; -/* For extensions XXX: we just drop the data */ -unknown_stmt : ustring ':' ustring ';' +/* Represents the usage of an extension + unknown-statement = prefix ":" identifier [sep string] optsep + (";" / + "{" optsep + *((yang-stmt / unknown-statement) optsep) + "}") stmt + * + */ +unknown_stmt : ustring ':' ustring optsep ';' { char *id; if ((id=string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); if (ysp_add(_yy, Y_UNKNOWN, id, NULL) == NULL) _YYERROR("unknown_stmt"); clicon_debug(2,"unknown-stmt -> ustring : ustring"); } - | ustring ':' ustring string ';' + | ustring ':' ustring SEP string optsep ';' { char *id; if ((id=string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); - if (ysp_add(_yy, Y_UNKNOWN, id, $4) == NULL){ _YYERROR("unknwon_stmt"); } + if (ysp_add(_yy, Y_UNKNOWN, id, $5) == NULL){ _YYERROR("unknwon_stmt"); } clicon_debug(2,"unknown-stmt -> ustring : ustring string"); } - | ustring ':' ustring + | ustring ':' ustring optsep { char *id; if ((id=string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); if (ysp_add_push(_yy, Y_UNKNOWN, id, NULL) == NULL) _YYERROR("unknown_stmt"); } '{' yang_stmts '}' { if (ystack_pop(_yy) < 0) _YYERROR("unknown_stmt"); clicon_debug(2,"unknown-stmt -> ustring : ustring { yang-stmts }"); } - | ustring ':' ustring string + | ustring ':' ustring SEP string optsep { char *id; if ((id=string_del_join($1, ":", $3)) == NULL) _YYERROR("unknown_stmt"); - if (ysp_add_push(_yy, Y_UNKNOWN, id, $4) == NULL) _YYERROR("unknown_stmt"); } + if (ysp_add_push(_yy, Y_UNKNOWN, id, $5) == NULL) _YYERROR("unknown_stmt"); } '{' yang_stmts '}' { if (ystack_pop(_yy) < 0) _YYERROR("unknown_stmt"); clicon_debug(2,"unknown-stmt -> ustring : ustring string { yang-stmts }"); } @@ -1815,6 +1823,11 @@ node_identifier : IDENTIFIER identifier_ref : node_identifier { $$=$1;} ; +optsep : SEP + | + ; + + stmtend : ';' | '{' '}' | '{' unknown_stmt '}' diff --git a/test/lib.sh b/test/lib.sh index 5f4ed32d..5daa83b8 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -34,7 +34,7 @@ if [ -f ./site.sh ]; then # test skiplist. for f in $SKIPLIST; do if [ "$testfile" = "$f" ]; then - echo ...skipped + echo "...skipped (see site.sh)" return -1 # skip fi done @@ -233,9 +233,11 @@ new(){ # Arguments: # - command, # - expected command return value (0 if OK) -# - expected stdout outcome, -# - expected2 stdout outcome, -# Example: expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 "^$" +# - expected* stdout outcome, (can be many) +# Example: expectfn "$clixon_cli -1 -f $cfg show conf cli" 0 "line1" "line2" +# XXX: for some reason some curl commands dont work here, eg +# curl -H 'Accept: application/xrd+xml' +# instead use expectpart expectfn(){ cmd=$1 retval=$2 @@ -258,32 +260,30 @@ expectfn(){ echo -e "\e[0m:" exit -1 fi -# if [ $r != 0 ]; then -# return -# fi # if [ $ret -ne $retval ]; then # echo -e "\e[31m\nError in Test$testnr [$testname]:" # echo -e "\e[0m:" # exit -1 # fi - # Match if both are empty string + # Match if both are empty string (special case) if [ -z "$ret" -a -z "$expect" ]; then return fi if [ -z "$ret" -a "$expect" = "^$" ]; then return fi - # grep extended grep - match=`echo $ret | grep -EZo "$expect"` - if [ -z "$match" ]; then - err "$expect" "$ret" - fi - if [ -n "$expect2" ]; then - match=`echo "$ret" | grep -EZo "$expect2"` - if [ -z "$match" ]; then - err $expect "$ret" + # Loop over all variable args expect strings + let i=0; + for exp in "$@"; do + if [ $i -gt 1 ]; then + match=`echo $ret | grep -EZo "$exp"` + if [ -z "$match" ]; then + err "$exp" "$ret" + fi fi - fi + let i++; + + done } # Evaluate and return @@ -313,6 +313,48 @@ expecteq(){ fi } +# Evaluate and return +# like expecteq but partial match is OK +# Example: expecteq $(fn arg) 0 "my return" +# - evaluated expression +# - expected command return value (0 if OK) +# - expected stdout outcome +expectpart(){ + r=$? + ret=$1 + retval=$2 + expect=$3 +# echo "r:$r" +# echo "ret:\"$ret\"" +# echo "retval:$retval" +# echo "expect:$expect" + if [ $r != $retval ]; then + echo -e "\e[31m\nError ($r != $retval) in Test$testnr [$testname]:" + echo -e "\e[0m:" + exit -1 + fi + if [ -z "$ret" -a -z "$expect" ]; then + return + fi + # Loop over all variable args expect strings + let i=0; + for exp in "$@"; do + if [ $i -gt 1 ]; then +# echo "exp:$exp" + match=`echo $ret | grep -Zo "$exp"` # XXX -EZo: -E cant handle {} + if [ -z "$match" ]; then + err "$exp" "$ret" + fi + fi + let i++; + done + +# if [[ "$ret" != "$expect" ]]; then +# err "$expect" "$ret" +# fi +} + + # Pipe stdin to command # Arguments: # - Command @@ -401,6 +443,43 @@ EOF fi } +# Like expecteof/expecteofx but with test == instead of grep. +# No wildcards +# Use this for multi-lines +expecteofeq(){ + cmd=$1 + retval=$2 + input=$3 + expect=$4 + +# Do while read stuff +ret=$($cmd<-23" - -new "json parse to json" # should be {"foo": -23} -expecteofx "$clixon_util_json -j" 0 '{"foo": -23}' '{"foo": "-23"}' - -new "json parse list xml" -expecteofx "$clixon_util_json" 0 '{"a":[0,1,2,3]}' "0123" - -new "json parse list json" # should be {"a":[0,1,2,3]} -expecteofx "$clixon_util_json -j" 0 '{"a":[0,1,2,3]}' '{"a": "0"}{"a": "1"}{"a": "2"}{"a": "3"}' - fyang=$dir/json.yang -fjson=$dir/json.json cat < $fyang module json{ prefix ex; @@ -39,20 +28,58 @@ module json{ } EOF -JSON='{"json:a": -23}' +new "test params: -y $fyang" + +# No yang +new "json parse to xml" +expecteofx "$clixon_util_json" 0 '{"foo": -23}' "-23" + +new "json parse to json" # should be {"foo": -23} +expecteofx "$clixon_util_json -j" 0 '{"foo": -23}' '{"foo":"-23"}' + +new "json parse list xml" +expecteofx "$clixon_util_json" 0 '{"a":[0,1,2,3]}' "0123" + +new "json parse list json" # should be {"a":[0,1,2,3]} +expecteofx "$clixon_util_json -j" 0 '{"a":[0,1,2,3]}' '{"a":"0"}{"a":"1"}{"a":"2"}{"a":"3"}' + +# Multi-line JOSN not pretty-print +JSON='{"json:c":{"a":42,"s":"string"}}' +# Same with pretty-print +JSONP='{ + "json:c": { + "a": 42, + "s": "string" + } +}' + +new "json no pp in/out" +expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" "$JSON" + +new "json pp in/out" +expecteofeq "$clixon_util_json -jpy $fyang" 0 "$JSONP" "$JSONP" + +new "json pp in/ no pp out" +expecteofeq "$clixon_util_json -jy $fyang" 0 "$JSONP" "$JSON" + +new "json no pp in/ pp out" +expecteofeq "$clixon_util_json -jpy $fyang" 0 "$JSON" "$JSONP" + +JSON='{"json:a":-23}' new "json leaf back to json" expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" -JSON='{"json:c": {"a": 937}}' +JSON='{"json:c":{"a":937}}' new "json parse container back to json" expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" -# This is wrong +# This should work if false; then JSON='{"json:c": {"s": " x & x < y ]]>"}}' new "json parse cdata xml" expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" fi + rm -rf $dir diff --git a/test/test_nacm.sh b/test/test_nacm.sh index c50308ab..982ddcd4 100755 --- a/test/test_nacm.sh +++ b/test/test_nacm.sh @@ -132,7 +132,7 @@ wait_backend wait_restconf new "auth get" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}} ' +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}} ' # explicitly disable nacm (regression on netgate bug) new "disable nacm" @@ -145,13 +145,13 @@ new "commit it" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "auth get (no user: access denied)" -expecteq "$(curl -sS -X GET -H \"Accept:\ application/yang-data+json\" http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "The requested URL was unauthorized"}}} ' +expecteq "$(curl -sS -X GET -H \"Accept:\ application/yang-data+json\" http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}} ' new "auth get (wrong passwd: access denied)" -expecteq "$(curl -u andy:foo -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "The requested URL was unauthorized"}}} ' +expecteq "$(curl -u andy:foo -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}} ' new "auth get (access)" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' #----------------Enable NACM @@ -160,24 +160,24 @@ new "enable nacm" expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": true}' http://localhost/restconf/data/ietf-netconf-acm:nacm/enable-nacm)" 0 "" new "admin get nacm" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' new "limited get nacm" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' new "guest get nacm" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "admin edit nacm" -expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" 0 "" +expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x":1}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "limited edit nacm" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest edit nacm" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index 5916ffa4..8da722c4 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -110,12 +110,12 @@ EOF #----------- First get case "$ret1" in - 0) ret='{"nacm-example:x": 42} + 0) ret='{"nacm-example:x":42} ' ;; - 1) ret='{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' + 1) ret='{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' ;; - 2) ret='{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}} ' + 2) ret='{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}} ' ;; esac @@ -126,7 +126,7 @@ EOF case "$ret2" in 0) ret='' ;; - 1) ret='{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' + 1) ret='{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' ;; esac new "edit new 99" @@ -134,14 +134,14 @@ EOF #----------- Then second get case "$ret3" in - 0) ret='{"nacm-example:x": 99} + 0) ret='{"nacm-example:x":99} ' ;; - 1) ret='{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' + 1) ret='{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' ;; - 2) ret='{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}} ' + 2) ret='{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}} ' ;; - 3) ret='{"nacm-example:x": 42} + 3) ret='{"nacm-example:x":42} ' esac new "get 99" diff --git a/test/test_nacm_ext.sh b/test/test_nacm_ext.sh index 102af693..d265d927 100755 --- a/test/test_nacm_ext.sh +++ b/test/test_nacm_ext.sh @@ -153,41 +153,41 @@ wait_backend wait_restconf new "auth get" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data)" 0 '{"data": {"clixon-example:state": {"op": ["42","41","43"]}}} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data)" 0 '{"data":{"clixon-example:state":{"op":["42","41","43"]}}} ' new "Set x to 0" expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 0}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "auth get (no user: access denied)" -expecteq "$(curl -sS -X GET -H \"Accept:\ application/yang-data+json\" http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "The requested URL was unauthorized"}}} ' +expecteq "$(curl -sS -X GET -H \"Accept:\ application/yang-data+json\" http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}} ' new "auth get (wrong passwd: access denied)" -expecteq "$(curl -u andy:foo -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "access-denied","error-severity": "error","error-message": "The requested URL was unauthorized"}}} ' +expecteq "$(curl -u andy:foo -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"access-denied","error-severity":"error","error-message":"The requested URL was unauthorized"}}} ' new "auth get (access)" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' new "admin get nacm" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' new "limited get nacm" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' new "guest get nacm" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "admin edit nacm" expecteq "$(curl -u andy:bar -sS -X PUT -d '{"nacm-example:x": 1}' http://localhost/restconf/data/nacm-example:x)" 0 "" new "limited edit nacm" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest edit nacm" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 3}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "cli show conf as admin" expectfn "$clixon_cli -1 -U andy -l o -f $cfg show conf" 0 "^x 1;$" diff --git a/test/test_nacm_module_read.sh b/test/test_nacm_module_read.sh index 1d87a9c1..f95cac52 100755 --- a/test/test_nacm_module_read.sh +++ b/test/test_nacm_module_read.sh @@ -160,27 +160,27 @@ expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": tru #----READ access #user:admin new "admin read ok" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"clixon-example:translate": [{"k": "key42","value": "val42"},{ "k": "key43","value": "val43"}]} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"clixon-example:translate":[{"k":"key42","value":"val42"},{"k":"key43","value":"val43"}]} ' new "admin read netconf ok" expecteof "$clixon_netconf -U andy -qf $cfg" 0 ']]>]]>' '^key42val42key43val43]]>]]>$' new "admin read element ok" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"clixon-example:value": "val42"} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"clixon-example:value":"val42"} ' new "admin read other module OK" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 42} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":42} ' new "admin read state OK" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state": {"op": ["42","41","43"]}} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} ' new "admin read top ok (all)" ret=$(curl -u andy:bar -sS -X GET http://localhost/restconf/data) -expect='{"data": {"nacm-example:x": 42,"clixon-example:translate":' +expect='{"data":{"nacm-example:x":42,"clixon-example:translate":' match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then err "$expect" "$ret" @@ -189,65 +189,65 @@ fi #user:limit new "limit read ok" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"clixon-example:translate": [{"k": "key42","value": "val42"},{ "k": "key43","value": "val43"}]} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"clixon-example:translate":[{"k":"key42","value":"val42"},{"k":"key43","value":"val43"}]} ' new "limit read netconf ok" expecteof "$clixon_netconf -U wilma -qf $cfg" 0 ']]>]]>' '^key42val42key43val43]]>]]>$' new "limit read element ok" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"clixon-example:value": "val42"} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"clixon-example:value":"val42"} ' new "limit read other module fail" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}} ' +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}} ' new "limit read state OK" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state": {"op": ["42","41","43"]}} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} ' new "limit read top ok (part)" -expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data)" 0 '{"data": {"clixon-example:translate": [{"k": "key42","value": "val42"},{ "k": "key43","value": "val43"}],"clixon-example:state": {"op": ["42","41","43"]}}} +expecteq "$(curl -u wilma:bar -sS -X GET http://localhost/restconf/data)" 0 '{"data":{"clixon-example:translate":[{"k":"key42","value":"val42"},{"k":"key43","value":"val43"}],"clixon-example:state":{"op":["42","41","43"]}}} ' #user:guest new "guest read fail" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest read netconf fail" expecteof "$clixon_netconf -U guest -qf $cfg" 0 ']]>]]>' '^applicationaccess-deniederrordefault deny]]>]]>$' new "guest read element fail" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:translate=key42/value)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest read other module fail" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest read state fail" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "guest read top ok (part)" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' #------- RPC operation new "admin rpc ok" -expecteq "$(curl -u andy:bar -s -X POST -d '{"clixon-example:input":{"x":"78"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "78","y": "42"}} +expecteq "$(curl -u andy:bar -s -X POST -d '{"clixon-example:input":{"x":"78"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"78","y":"42"}} ' new "admin rpc netconf ok" expecteof "$clixon_netconf -U andy -qf $cfg" 0 '0]]>]]>' 0 '^042]]>]]>$' new "limit rpc ok" -expecteq "$(curl -u wilma:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"clixon-example:output": {"x": "42","y": "42"}} +expecteq "$(curl -u wilma:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"clixon-example:output":{"x":"42","y":"42"}} ' new "limit rpc netconf ok" expecteof "$clixon_netconf -U wilma -qf $cfg" 0 '0]]>]]>' 0 '^042]]>]]>$' new "guest rpc fail" -expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -s -X POST http://localhost/restconf/operations/clixon-example:example -d '{"clixon-example:input":{"x":42}}' )" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "guest rpc netconf fail" expecteof "$clixon_netconf -U guest -qf $cfg" 0 '0]]>]]>' 0 '^applicationaccess-deniederroraccess denied]]>]]>$' @@ -255,18 +255,18 @@ expecteof "$clixon_netconf -U guest -qf $cfg" 0 '42$RULES')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d '42$RULES')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' # replace all, then must include NACM rules as well MSG="$RULES" @@ -186,7 +186,7 @@ new "update root list permit" expecteq "$(curl -u andy:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d "$MSG")" 0 '' new "delete root list deny" -expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "delete root permit" expecteq "$(curl -u andy:bar -sS -X DELETE http://localhost/restconf/data)" 0 '' @@ -196,62 +196,62 @@ nacm #----------leaf new "create leaf deny" -expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '42')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '42')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "create leaf permit" expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '42')" 0 '' new "update leaf deny" -expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '99')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '99')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "update leaf permit" expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:x -d '99')" 0 '' new "read leaf check" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 99} +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":99} ' new "delete leaf deny" -expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "delete leaf permit" expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:x)" 0 '' #----- list/container new "create list deny" -expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42str')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42str')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "create list permit" expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42str')" 0 '' new "update list deny" -expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42update')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42update')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "update list permit" expecteq "$(curl -u guest:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data/nacm-example:a=key42 -d 'key42update')" 0 '' new "read list check" -expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:a)" 0 '{"nacm-example:a": [{"k": "key42","b": {"c": "update"}}]} +expecteq "$(curl -u guest:bar -sS -X GET http://localhost/restconf/data/nacm-example:a)" 0 '{"nacm-example:a":[{"k":"key42","b":{"c":"update"}}]} ' new "delete list deny" -expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:a=key42)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "access denied"}}} ' +expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:a=key42)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"access denied"}}} ' new "delete list permit" expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/nacm-example:a=key42)" 0 '' #----- default deny (clixon-example limit and guest have default access) new "default create list deny" -expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k": "key42","value": "val42"}]}')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "create list permit" -expecteq "$(curl -u andy:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k": "key42","value": "val42"}]}')" 0 '' +expecteq "$(curl -u andy:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val42"}]}')" 0 '' new "default update list deny" -expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k": "key42","value": "val99"}]}')" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X PUT http://localhost/restconf/data/clixon-example:translate=key42 -d '{"clixon-example:translate": [{"k":"key42","value":"val99"}]}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "default delete list deny" -expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/clixon-example:translate=key42)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data/clixon-example:translate=key42)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_nacm_protocol.sh b/test/test_nacm_protocol.sh index 59ce33c7..ee063a57 100755 --- a/test/test_nacm_protocol.sh +++ b/test/test_nacm_protocol.sh @@ -167,7 +167,7 @@ expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": tru #--------------- nacm enabled new "admin get nacm" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x": 0} +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"nacm-example:x":0} ' # Rule 1: deny-kill-session @@ -185,14 +185,14 @@ new "deny-delete-config: limited fail (netconf)" expecteof "$clixon_netconf -qf $cfg -U wilma" 0 "]]>]]>" "^applicationaccess-deniederroraccess denied]]>]]>$" new "deny-delete-config: guest fail (restconf)" -expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X DELETE http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' # In restconf delete-config is translated to edit-config which is permitted new "deny-delete-config: limited fail (restconf) ok" expecteq "$(curl -u wilma:bar -sS -X DELETE http://localhost/restconf/data)" 0 '' new "admin get nacm (should fail)" -expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}} ' +expecteq "$(curl -u andy:bar -sS -X GET http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}} ' new "deny-delete-config: admin ok (restconf)" expecteq "$(curl -u andy:bar -sS -X DELETE http://localhost/restconf/data)" 0 '' @@ -209,10 +209,10 @@ expecteq "$(curl -u andy:bar -sS -X PUT -d '{"ietf-netconf-acm:enable-nacm": tru # Rule 3: permit-edit-config new "permit-edit-config: limited ok restconf" -expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '' +expecteq "$(curl -u wilma:bar -sS -X PUT -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '' new "permit-edit-config: guest fail restconf" -expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x": 2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "access-denied","error-severity": "error","error-message": "default deny"}}} ' +expecteq "$(curl -u guest:bar -sS -X PUT -d '{"nacm-example:x":2}' http://localhost/restconf/data/nacm-example:x)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_perf_state.sh b/test/test_perf_state.sh index 1449f7d4..17d3ef1b 100755 --- a/test/test_perf_state.sh +++ b/test/test_perf_state.sh @@ -99,7 +99,7 @@ done | $clixon_netconf -qf $cfg > /dev/null; } 2>&1 | awk '/real/ {print $2}' # RESTCONF get new "restconf get test single req XXX" -expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface": [{"name": "e1","type": "ex:eth","enabled": true,"oper-status": "up"}]} +expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface":[{"name":"e1","type":"ex:eth","enabled":true,"oper-status":"up"}]} ' new "restconf get $perfreq single reqs" diff --git a/test/test_restconf.sh b/test/test_restconf.sh index fe86cc65..fd4aecbd 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -34,7 +34,7 @@ cat < $cfg EOF # This is a fixed 'state' implemented in routing_backend. It is assumed to be always there -state='{"clixon-example:state": {"op": ["42","41","43"]}} ' +state='{"clixon-example:state":{"op":\["42","41","43"\]}' new "test params: -f $cfg -- -s" if [ $BE -ne 0 ]; then @@ -64,12 +64,12 @@ expecteq "$(curl -s -X GET http://localhost/.well-known/host-meta)" 0 " " new "restconf get restconf resource. RFC 8040 3.3 (json)" -expecteq "$(curl -sG http://localhost/restconf)" 0 '{"restconf": {"data": null,"operations": null,"yang-library-version": "2016-06-21"}} +expecteq "$(curl -sG http://localhost/restconf)" 0 '{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}} ' new "restconf get restconf resource. RFC 8040 3.3 (xml)" # Get XML instead of JSON? -expecteq "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/restconf)" 0 '2016-06-21 +expecteq "$(curl -s -H 'Accept: application/yang-data+xml' -G http://localhost/restconf)" 0 '2016-06-21 ' # Should be alphabetically ordered @@ -86,7 +86,7 @@ if [ -z "$match" ]; then fi new "restconf get restconf/yang-library-version. RFC8040 3.3.3" -expecteq "$(curl -sG http://localhost/restconf/yang-library-version)" 0 '{"yang-library-version": "2016-06-21"}' +expecteq "$(curl -sG http://localhost/restconf/yang-library-version)" 0 '{"yang-library-version":"2016-06-21"}' new "restconf get restconf/yang-library-version. RFC8040 3.3.3 (xml)" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/yang-library-version) @@ -97,7 +97,7 @@ if [ -z "$match" ]; then fi new "restconf schema resource, RFC 8040 sec 3.7 according to RFC 7895 (explicit resource)" -expecteq "$(curl -s -H 'Accept: application/yang-data+json' -G http://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 '{"ietf-yang-library:module": [{"name": "ietf-interfaces","revision": "2018-02-20","namespace": "urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type": "implement"}]} +expecteq "$(curl -s -H 'Accept: application/yang-data+json' -G http://localhost/restconf/data/ietf-yang-library:modules-state/module=ietf-interfaces,2018-02-20)" 0 '{"ietf-yang-library:module":[{"name":"ietf-interfaces","revision":"2018-02-20","namespace":"urn:ietf:params:xml:ns:yang:ietf-interfaces","conformance-type":"implement"}]} ' new "restconf options. RFC 8040 4.1" @@ -111,14 +111,14 @@ new "restconf empty rpc" expecteq "$(curl -s -X POST -d {\"clixon-example:input\":null} http://localhost/restconf/operations/clixon-example:empty)" 0 "" new "restconf empty rpc with extra args (should fail)" -expecteq "$(curl -s -X POST -d {\"clixon-example:input\":{\"extra\":null}} http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "unknown-element","error-info": {"bad-element": "extra"},"error-severity": "error"}}} ' +expecteq "$(curl -s -X POST -d {\"clixon-example:input\":{\"extra\":null}} http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error"}}} ' new "restconf get empty config + state json" -expecteq "$(curl -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state": {"op": ["42","41","43"]}} +expecteq "$(curl -sS -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} ' new "restconf get empty config + state json with wrong module name" -expecteq "$(curl -sSG http://localhost/restconf/data/badmodule:state)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "No such yang module: badmodule"}}} ' +expecteq "$(curl -sSG http://localhost/restconf/data/badmodule:state)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"No such yang module: badmodule"}}} ' new "restconf get empty config + state xml" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/clixon-example:state) @@ -129,7 +129,7 @@ if [ -z "$match" ]; then fi new "restconf get data/ json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op": ["42","41","43"]} +expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":["42","41","43"]} ' new "restconf get state operation" @@ -142,7 +142,7 @@ if [ -z "$match" ]; then fi new "restconf get state operation type json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op": ["42","41","43"]} +expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":["42","41","43"]} ' new "restconf get state operation type xml" @@ -155,7 +155,7 @@ if [ -z "$match" ]; then fi new "restconf GET datastore" -expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state": {"op": ["42","41","43"]}} +expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} ' # Exact match @@ -163,13 +163,13 @@ new "restconf Add subtree eth/0/0 to datastore using POST" expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 200 OK' new "restconf Re-add subtree eth/0/0 which should give error" -expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-exists","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' # XXX Cant get this to work -#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"ex:eth\",\"enabled\":true}}} http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-exists","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}}' +#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"ex:eth\",\"enabled\":true}}} http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' new "restconf Check interfaces eth/0/0 added" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces": {"interface": [{"name": "eth/0/0","type": "ex:eth","enabled": true,"oper-status": "up"}]}}} +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}}} ' new "restconf delete interfaces" @@ -187,60 +187,60 @@ expectfn 'curl -s -X POST -d {"ietf-interfaces:interface":{"name":"eth/0/0","typ #expecteq "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"ex:eth","enabled":true}}' http://localhost/restconf/data/interfaces)" 0 "" new "restconf Check eth/0/0 added config" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces": {"interface": [{"name": "eth/0/0","type": "ex:eth","enabled": true,"oper-status": "up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf Check eth/0/0 added state" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state": {"op": ["42","41","43"]}} +expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state)" 0 '{"clixon-example:state":{"op":["42","41","43"]}} ' new "restconf Re-post eth/0/0 which should generate error" -expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-exists","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "Add leaf description using POST" expecteq "$(curl -s -X POST -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "Add nothing using POST" -expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": " on line 1: syntax error at or before:' +expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":" on line 1: syntax error at or before:' new "restconf Check description added" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces": {"interface": [{"name": "eth/0/0","description": "The-first-interface","type": "ex:eth","enabled": true,"oper-status": "up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"ex:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf delete eth/0/0" expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "Check deleted eth/0/0" -expectfn 'curl -s -G http://localhost/restconf/data' 0 $state +expectfn 'curl -s -G http://localhost/restconf/data' 0 "$state" new "restconf Re-Delete eth/0/0 using none should generate error" -expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-missing","error-severity": "error","error-message": "Data does not exist; cannot delete resource"}}} ' +expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}} ' new "restconf Add subtree eth/0/0 using PUT" expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "restconf get subtree" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces": {"interface": [{"name": "eth/0/0","type": "ex:eth","enabled": true,"oper-status": "up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf rpc using POST json" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "42","y": "42"}} +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"42","y":"42"}} ' new "restconf rpc using POST json wrong" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "unknown-element","error-info": {"bad-element": "wrongelement"},"error-severity": "error"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"wrongelement":"ipv4"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"wrongelement"},"error-severity":"error"}}} ' new "restconf rpc non-existing rpc without namespace" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/kalle)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "kalle"},"error-severity": "error","error-message": "RPC not defined"}}} ' +expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' new "restconf rpc non-existing rpc" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:kalle)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "kalle"},"error-severity": "error","error-message": "RPC not defined"}}} ' +expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:kalle)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"kalle"},"error-severity":"error","error-message":"RPC not defined"}}} ' new "restconf rpc missing name" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "Operation name expected"}}} ' +expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"Operation name expected"}}} ' new "restconf rpc missing input" -expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "restconf RPC does not have input statement"}}} ' +expecteq "$(curl -s -X POST -d '{}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"restconf RPC does not have input statement"}}} ' new "restconf rpc using POST xml" ret=$(curl -s -X POST -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":42}}' http://localhost/restconf/operations/clixon-example:example) @@ -251,7 +251,7 @@ if [ -z "$match" ]; then fi new "restconf rpc using wrong prefix" -expecteq "$(curl -s -X POST -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/wrong:example)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "yang module not found"}}} ' +expecteq "$(curl -s -X POST -d '{"wrong:input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/wrong:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"yang module not found"}}} ' new "restconf local client rpc using POST xml" ret=$(curl -s -i -X POST -H "Accept: application/yang-data+xml" -d '{"clixon-example:input":{"x":"example"}}' http://localhost/restconf/operations/clixon-example:client-rpc) @@ -262,10 +262,10 @@ if [ -z "$match" ]; then fi new "restconf Add subtree without key (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "malformed key, expected '"'"'=restval'"'"'"}}} ' +expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}} ' new "restconf Add subtree with too many keys (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "List key interface length mismatch"}}} ' +expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh index ec1486d7..ba39e427 100755 --- a/test/test_restconf2.sh +++ b/test/test_restconf2.sh @@ -90,19 +90,19 @@ wait_backend wait_restconf new "restconf POST tree without key" -expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "name"},"error-severity": "error","error-message": "Mandatory key"}}} ' +expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' new "restconf POST initial tree" expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 "" new "restconf POST top without namespace" -expectfn 'curl -s -X POST -d {"cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "unknown-element","error-info": {"bad-element": "cont1"},"error-severity": "error","error-message": "Unassigned yang spec"}}}' +expectfn 'curl -s -X POST -d {"cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"cont1"},"error-severity":"error","error-message":"Unassigned yang spec"}}}' new "restconf GET datastore initial" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1": {"interface": \[{"name": "local0","type": "regular"}\]}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf GET interface subtree" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface": \[{"name": "local0","type": "regular"}\]}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"regular"}\]}' new "restconf GET interface subtree xml" ret=$(curl -s -H "Accept: application/yang-data+xml" -G http://localhost/restconf/data/example:cont1/interface=local0) @@ -113,55 +113,55 @@ if [ -z "$match" ]; then fi new "restconf GET if-type" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0/type" 0 '{"example:type": "regular"}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0/type" 0 '{"example:type":"regular"}' new "restconf POST interface without mandatory type" -expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"name":"TEST"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "type"},"error-severity": "error","error-message": "Mandatory variable"}}} ' +expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"name":"TEST"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"type"},"error-severity":"error","error-message":"Mandatory variable"}}} ' new "restconf POST interface without mandatory key" -expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"type":"regular"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "name"},"error-severity": "error","error-message": "Mandatory key"}}} ' +expectfn 'curl -s -X POST http://localhost/restconf/data/example:cont1 -d {"example:interface":{"type":"regular"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"name"},"error-severity":"error","error-message":"Mandatory key"}}} ' new "restconf POST interface" expectfn 'curl -s -X POST -d {"example:interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/example:cont1' 0 "" new "restconf POST interface without namespace" -expectfn 'curl -s -X POST -d {"interface":{"name":"TEST2","type":"eth0"}} http://localhost/restconf/data/example:cont1' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "Data is not prefixed with matching namespace"}}}' +expectfn 'curl -s -X POST -d {"interface":{"name":"TEST2","type":"eth0"}} http://localhost/restconf/data/example:cont1' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}}' new "restconf POST again" -expecteq "$(curl -s -X POST -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-exists","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -d '{"example:interface":{"name":"TEST","type":"eth0"}}' http://localhost/restconf/data/example:cont1)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "restconf POST from top" -expecteq "$(curl -s -X POST -d '{"example:cont1":{"interface":{"name":"TEST","type":"eth0"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "data-exists","error-severity": "error","error-message": "Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -d '{"example:cont1":{"interface":{"name":"TEST","type":"eth0"}}}' http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "restconf DELETE" expectfn 'curl -s -X DELETE http://localhost/restconf/data/example:cont1' 0 "" new "restconf GET null datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}}' new "restconf POST initial tree" expectfn 'curl -s -X POST -d {"example:cont1":{"interface":{"name":"local0","type":"regular"}}} http://localhost/restconf/data' 0 "" new "restconf GET initial tree" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1": {"interface": \[{"name": "local0","type": "regular"}\]}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new "restconf GET null datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}}' new "restconf PUT initial datastore" expectfn 'curl -s -X PUT -d {"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}} http://localhost/restconf/data' 0 "" new "restconf GET datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1": {"interface": \[{"name": "local0","type": "regular"}\]}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"regular"}\]}}' new "restconf PUT replace datastore" expectfn 'curl -s -X PUT -d {"data":{"example:cont2":{"name":"foo"}}} http://localhost/restconf/data' 0 "" new "restconf GET replaced datastore" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont2" 0 '{"example:cont2": {"name": "foo"}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont2" 0 '{"example:cont2":{"name":"foo"}}' new "restconf PUT initial datastore again" expectfn 'curl -s -X PUT -d {"data":{"example:cont1":{"interface":{"name":"local0","type":"regular"}}}} http://localhost/restconf/data' 0 "" @@ -170,26 +170,26 @@ new "restconf PUT change interface" expectfn 'curl -s -X PUT -d {"example:interface":{"name":"local0","type":"atm0"}} http://localhost/restconf/data/example:cont1/interface=local0' 0 "" new "restconf GET datastore atm" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1": {"interface": \[{"name": "local0","type": "atm0"}\]}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1" 0 '{"example:cont1":{"interface":\[{"name":"local0","type":"atm0"}\]}}' new "restconf PUT add interface" expectfn 'curl -s -X PUT -d {"example:interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/example:cont1/interface=TEST' 0 "" new "restconf PUT change key error" -expectfn 'curl -is -X PUT -d {"example:interface":{"name":"ALPHA","type":"eth0"}} http://localhost/restconf/data/example:cont1/interface=TEST' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -is -X PUT -d {"example:interface":{"name":"ALPHA","type":"eth0"}} http://localhost/restconf/data/example:cont1/interface=TEST' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT change type to eth0 (non-key sub-element to list)" expectfn 'curl -s -X PUT -d {"example:type":"eth0"} http://localhost/restconf/data/example:cont1/interface=local0/type' 0 "" new "restconf GET datastore eth" -expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface": \[{"name": "local0","type": "eth0"}\]}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:cont1/interface=local0" 0 '{"example:interface":\[{"name":"local0","type":"eth0"}\]}' #--------------- json type tests new "restconf POST type x3" expectfn 'curl -s -X POST -d {"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}} http://localhost/restconf/data' 0 '' new "restconf POST type x3" -expectfn 'curl -s -X GET http://localhost/restconf/data/example:types' 0 '{"example:types": {"tint": 42,"tdec64": 42.123,"tbool": false,"tstr": "str"}}' +expectfn 'curl -s -X GET http://localhost/restconf/data/example:types' 0 '{"example:types":{"tint":42,"tdec64":42.123,"tbool":false,"tstr":"str"}}' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf_err.sh b/test/test_restconf_err.sh index fb5e27da..8c218c29 100755 --- a/test/test_restconf_err.sh +++ b/test/test_restconf_err.sh @@ -99,7 +99,7 @@ new "restconf GET non-existent container header" expectfn "curl -s -I -X GET http://localhost/restconf/data/example:a/c" 0 "HTTP/1.1 404 Not Found" new "restconf GET non-existent container body" -expectfn "curl -s -X GET http://localhost/restconf/data/example:a/c" 0 '{"ietf-restconf:errors" : {"error": {"rpc-error": {"error-type": "application","error-tag": "invalid-value","error-severity": "error","error-message": "Instance does not exist"}}}}' +expectfn "curl -s -X GET http://localhost/restconf/data/example:a/c" 0 '{"ietf-restconf:errors":{"error":{"rpc-error":{"error-type":"application","error-tag":"invalid-value","error-severity":"error","error-message":"Instance does not exist"}}}}' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh new file mode 100755 index 00000000..9cabb0b2 --- /dev/null +++ b/test/test_restconf_jukebox.sh @@ -0,0 +1,364 @@ +#!/bin/bash +# Restconf RFC8040 Appendix A and B "jukebox" example + +# 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.xml +fyang=$dir/restconf.yang +fxml=$dir/initial.xml + +# example +cat < $cfg + + $cfg + /usr/local/share/clixon + $IETFRFC + $fyang + false + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/lib/$APPNAME/backend + $dir/restconf.pidfile + /usr/local/var/$APPNAME + true + +EOF + +cat < $fyang + module example-jukebox { + + namespace "http://example.com/ns/example-jukebox"; + prefix "jbox"; + + organization "Example, Inc."; + contact "support at example.com"; + description "Example Jukebox Data Model Module."; + revision "2016-08-15" { + description "Initial version."; + reference "example.com document 1-4673."; + } + + identity genre { + description + "Base for all genre types."; + } + + // abbreviated list of genre classifications + identity alternative { + base genre; + description + "Alternative music."; + } + identity blues { + base genre; + description + "Blues music."; + } + identity country { + base genre; + description + "Country music."; + } + identity jazz { + base genre; + description + "Jazz music."; + } + identity pop { + base genre; + description + "Pop music."; + } + identity rock { + base genre; + description + "Rock music."; + } + + container jukebox { + presence + "An empty container indicates that the jukebox + service is available."; + + description + "Represents a 'jukebox' resource, with a library, playlists, + and a 'play' operation."; + + container library { + + description + "Represents the 'jukebox' library resource."; + + list artist { + key name; + description + "Represents one 'artist' resource within the + 'jukebox' library resource."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the artist."; + } + + list album { + key name; + description + "Represents one 'album' resource within one + 'artist' resource, within the jukebox library."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the album."; + } + + leaf genre { + type identityref { base genre; } + description + "The genre identifying the type of music on + the album."; + } + + leaf year { + type uint16 { + range "1900 .. max"; + } + description + "The year the album was released."; + } + + container admin { + description + "Administrative information for the album."; + + leaf label { + type string; + description + "The label that released the album."; + } + leaf catalogue-number { + type string; + description + "The album's catalogue number."; + } + } + + list song { + key name; + description + "Represents one 'song' resource within one + 'album' resource, within the jukebox library."; + + leaf name { + type string { + length "1 .. max"; + } + description + "The name of the song."; + } + + leaf location { + type string; + mandatory true; + description + "The file location string of the + media file for the song."; + } + leaf format { + type string; + description + "An identifier string for the media type + for the file associated with the + 'location' leaf for this entry."; + } + leaf length { + type uint32; + units "seconds"; + description + "The duration of this song in seconds."; + } + } // end list 'song' + } // end list 'album' + } // end list 'artist' + + leaf artist-count { + type uint32; + units "artists"; + config false; + description + "Number of artists in the library."; + } + leaf album-count { + type uint32; + units "albums"; + config false; + description + "Number of albums in the library."; + } + leaf song-count { + type uint32; + units "songs"; + config false; + description + "Number of songs in the library."; + } + } // end library + + list playlist { + key name; + description + "Example configuration data resource."; + + leaf name { + type string; + description + "The name of the playlist."; + } + leaf description { + type string; + description + "A comment describing the playlist."; + } + list song { + key index; + ordered-by user; + + description + "Example nested configuration data resource."; + + leaf index { // not really needed + type uint32; + description + "An arbitrary integer index for this playlist song."; + } + leaf id { + type instance-identifier; + mandatory true; + description + "Song identifier. Must identify an instance of + /jukebox/library/artist/album/song/name."; + } + } + } + + container player { + description + "Represents the jukebox player resource."; + + leaf gap { + type decimal64 { + fraction-digits 1; + range "0.0 .. 2.0"; + } + units "tenths of seconds"; + description + "Time gap between each song."; + } + } + } + + rpc play { + description + "Control function for the jukebox player."; + input { + leaf playlist { + type string; + mandatory true; + description + "The playlist name."; + } + leaf song-number { + type uint32; + mandatory true; + description + "Song number in playlist to play."; + } + } + } + } +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 + sudo pkill clixon_backend # to be sure + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg +fi + +new "kill old restconf daemon" +sudo pkill -u www-data -f "/www-data/clixon_restconf" + +new "start restconf daemon" +start_restconf -f $cfg + +new "waiting" +wait_backend +wait_restconf + +new "B.1.1. Retrieve the Top-Level API Resource root" +expectpart "$(curl -s -i -X GET -H 'Accept: application/xrd+xml' http://localhost/.well-known/host-meta)" 0 "HTTP/1.1 200 OK" "Content-Type: application/xrd+xml" "" "" "" + +d='{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}}' +new "B.1.1. Retrieve the Top-Level API Resource /restconf json" +expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" "$d" + +new "B.1.1. Retrieve the Top-Level API Resource /restconf xml (not in RFC)" +expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+xml" '2016-06-21' + +# This just catches the header and the jukebox module, the RFC has foo and bar which +# seems wrong to recreate +new "B.1.2. Retrieve the Server Module Information" +expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":' '"module":\[{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"}' + +new "B.1.3. Retrieve the Server Capability Information" +expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit' + +if false; then # NYI +new "B.2.1. Create New Data Resources" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location:" # etc + +new "B.2.2. Detect Datastore Resource Entity-Tag Change" +new "B.2.3. Edit a Datastore Resource" +new "B.2.4. Replace a Datastore Resource" +new "B.2.5. Edit a Data Resource" +new 'B.3.1. "content" Parameter' +new 'B.3.2. "depth" Parameter' +new 'B.3.3. "fields" Parameter' +new 'B.3.4. "insert" Parameter' +new 'B.3.5. "point" Parameter' +new 'B.3.6. "filter" Parameter' +new 'B.3.7. "start-time" Parameter' +new 'B.3.8. "stop-time" Parameter' +new 'B.3.9. "with-defaults" Parameter' +fi + +new "Kill restconf daemon" +stop_restconf + +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_restconf_listkey.sh b/test/test_restconf_listkey.sh index cb0546e4..294f18f6 100755 --- a/test/test_restconf_listkey.sh +++ b/test/test_restconf_listkey.sh @@ -98,16 +98,16 @@ new "restconf PUT change whole list entry (same keys)" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"x","c":"y","nonkey":"z"}}' 0 '' new "restconf PUT change whole list entry (no namespace)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"a":{"b":"x","c":"y","nonkey":"z"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "Data is not prefixed with matching namespace"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"a":{"b":"x","c":"y","nonkey":"z"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"Data is not prefixed with matching namespace"}}}' new "restconf PUT change list entry (wrong keys)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"y","c":"x"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y -d {"list:a":{"b":"y","c":"x"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT change list entry (wrong keys)(expect fail) XML" expecteq "$(curl -s -X PUT -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+xml' -d 'xyxz0' http://localhost/restconf/data/list:c/a=xx,xy)" 0 'protocoloperation-failederrorapi-path keys do not match data keys ' new "restconf PUT change list entry (just one key)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x -d {"list:a":{"b":"x"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "List key a length mismatch"}}} ' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x -d {"list:a":{"b":"x"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}} ' new "restconf PUT sub non-key" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/nonkey -d {"list:nonkey":"u"}' 0 '' @@ -116,37 +116,37 @@ new "restconf PUT sub key same value" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/b -d {"list:b":"x"}' 0 '' new "restconf PUT just key other value (should fail)ZX" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/b -d {"b":"y"}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "List key a length mismatch"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/b -d {"b":"y"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}}' new "restconf PUT add leaf-list entry" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/d=x -d {"list:d":"x"}' 0 '' new "restconf PUT change leaf-list entry (expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/d=x -d {"list:d":"y"}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/d=x -d {"list:d":"y"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT list-list" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z -d {"list:e":{"f":"z","nonkey":"0"}}' 0 '' new "restconf PUT change list-lst entry (wrong keys)(expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z -d {"list:e":{"f":"wrong","nonley":"0"}}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z -d {"list:e":{"f":"wrong","nonley":"0"}}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT list-list sub non-key" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/nonkey -d {"list:nonkey":"u"}' 0 '' new "restconf PUT list-list single first key" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/e=z/f -d {"f":"z"}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "rpc","error-tag": "malformed-message","error-severity": "error","error-message": "List key a length mismatch"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x/e=z/f -d {"f":"z"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key a length mismatch"}}}' new "restconf PUT list-list just key ok" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/f -d {"list:f":"z"}' 0 '' new "restconf PUT list-list just key just key wrong value (should fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/f -d {"list:f":"wrong"}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/e=z/f -d {"list:f":"wrong"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "restconf PUT add list+leaf-list entry" expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/f=u -d {"list:f":"u"}' 0 '' new "restconf PUT change list+leaf-list entry (expect fail)" -expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/f=u -d {"list:f":"w"}' 0 '{"ietf-restconf:errors" : {"error": {"error-type": "protocol","error-tag": "operation-failed","error-severity": "error","error-message": "api-path keys do not match data keys"}}}' +expectfn 'curl -s -X PUT http://localhost/restconf/data/list:c/a=x,y/f=u -d {"list:f":"w"}' 0 '{"ietf-restconf:errors":{"error":{"error-type":"protocol","error-tag":"operation-failed","error-severity":"error","error-message":"api-path keys do not match data keys"}}}' new "Kill restconf daemon" diff --git a/test/test_rpc.sh b/test/test_rpc.sh index 8190608a..0c956e03 100755 --- a/test/test_rpc.sh +++ b/test/test_rpc.sh @@ -70,20 +70,20 @@ new "netconf empty rpc" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' new "restconf example rpc json/json default - no http media headers" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "0","y": "42"}} +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc json/json change y default" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0","y":"99"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "0","y": "99"}} +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0","y":"99"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"99"}} ' new "restconf example rpc json/json" # XXX example:input example:output -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+json' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "0","y": "42"}} +expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+json' -H 'Accept: application/yang-data+json' -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc xml/json" -expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+json' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output": {"x": "0","y": "42"}} +expecteq "$(curl -s -X POST -H 'Content-Type: application/yang-data+xml' -H 'Accept: application/yang-data+json' -d '0' http://localhost/restconf/operations/clixon-example:example)" 0 '{"clixon-example:output":{"x":"0","y":"42"}} ' new "restconf example rpc json/xml" @@ -115,7 +115,7 @@ if [ -z "$match" ]; then fi new "restconf empty rpc with input x" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "unknown-element","error-info": {"bad-element": "x"},"error-severity": "error"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":0}}' http://localhost/restconf/operations/clixon-example:empty)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"x"},"error-severity":"error"}}} ' # cornercase: optional has yang input/output sections but test without body new "restconf optional rpc with null input and output" @@ -127,16 +127,16 @@ if [ -z "$match" ]; then fi new "restconf omit mandatory" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":null}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "x"},"error-severity": "error","error-message": "Mandatory variable"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":null}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"x"},"error-severity":"error","error-message":"Mandatory variable"}}} ' new "restconf add extra" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0","extra":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "unknown-element","error-info": {"bad-element": "extra"},"error-severity": "error"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0","extra":"0"}}' http://localhost/restconf/operations/clixon-example:example)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"extra"},"error-severity":"error"}}} ' new "restconf wrong method" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:wrong)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "wrong"},"error-severity": "error","error-message": "RPC not defined"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":{"x":"0"}}' http://localhost/restconf/operations/clixon-example:wrong)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"wrong"},"error-severity":"error","error-message":"RPC not defined"}}} ' new "restconf example missing input" -expecteq "$(curl -s -X POST -d '{"clixon-example:input":null}' http://localhost/restconf/operations/ietf-netconf:edit-config)" 0 '{"ietf-restconf:errors" : {"error": {"error-type": "application","error-tag": "missing-element","error-info": {"bad-element": "target"},"error-severity": "error","error-message": "Mandatory variable"}}} ' +expecteq "$(curl -s -X POST -d '{"clixon-example:input":null}' http://localhost/restconf/operations/ietf-netconf:edit-config)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"missing-element","error-info":{"bad-element":"target"},"error-severity":"error","error-message":"Mandatory variable"}}} ' new "netconf kill-session missing session-id mandatory" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^applicationmissing-elementsession-iderrorMandatory variable]]>]]>$' diff --git a/test/test_stream.sh b/test/test_stream.sh index 2f8af4bd..a4dcd703 100755 --- a/test/test_stream.sh +++ b/test/test_stream.sh @@ -165,11 +165,11 @@ sleep 2 new "2. Restconf RFC8040 stream testing" # 2.1 Stream discovery new "restconf event stream discovery RFC8040 Sec 6.2" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams" 0 '{"ietf-restconf-monitoring:streams": {"stream": \[{"name": "EXAMPLE","description": "Example event stream","replay-support": true,"access": \[{"encoding": "xml","location": "https://localhost/streams/EXAMPLE"}\]}\]}' +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams" 0 '{"ietf-restconf-monitoring:streams":{"stream":\[{"name":"EXAMPLE","description":"Example event stream","replay-support":true,"access":\[{"encoding":"xml","location":"https://localhost/streams/EXAMPLE"}\]}\]}' sleep 2 new "restconf subscribe RFC8040 Sec 6.3, get location" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams/stream=EXAMPLE/access=xml/location" 0 '{"ietf-restconf-monitoring:location": "https://localhost/streams/EXAMPLE"}' +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams/stream=EXAMPLE/access=xml/location" 0 '{"ietf-restconf-monitoring:location":"https://localhost/streams/EXAMPLE"}' sleep 2 # Restconf stream subscription RFC8040 Sec 6.3 diff --git a/test/test_submodule.sh b/test/test_submodule.sh index fc8711d1..d64812b3 100755 --- a/test/test_submodule.sh +++ b/test/test_submodule.sh @@ -213,7 +213,7 @@ new "restconf edit sub2" expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 200 OK' new "restconf check main/sub1/sub2 contents" -expectfn "curl -s -X GET http://localhost/restconf/data" 0 '{"data": {"main:main": {"ext": "foo","x": "foo"},"main:sub1": {"ext1": "foo","x": "foo"},"main:sub2": {"ext2": "foo","x": "foo"}}}' +expectfn "curl -s -X GET http://localhost/restconf/data" 0 '{"data":{"main:main":{"ext":"foo","x":"foo"},"main:sub1":{"ext1":"foo","x":"foo"},"main:sub2":{"ext2":"foo","x":"foo"}}}' new "Kill restconf daemon" stop_restconf diff --git a/test/test_xml.sh b/test/test_xml.sh index f24997ac..c3943ea5 100755 --- a/test/test_xml.sh +++ b/test/test_xml.sh @@ -12,7 +12,7 @@ new "xml parse" expecteof "$clixon_util_xml" 0 "" "^$" new "xml parse to json" -expecteof "$clixon_util_xml -j" 0 "" '{"a": {"b": null}}' +expecteof "$clixon_util_xml -j" 0 "" '{"a":{"b":null}}' new "xml parse strange names" expecteof "$clixon_util_xml" 0 "<_->" "<_->" @@ -38,7 +38,7 @@ new "xml simple CDATA" expecteofx "$clixon_util_xml" 0 '' '' new "xml simple CDATA to json" -expecteofx "$clixon_util_xml -j" 0 '' '{"a": "a text"}' +expecteofx "$clixon_util_xml -j" 0 '' '{"a":"a text"}' new "xml complex CDATA" XML=$(cat <An example of escaped CE ]]>]]>$" JSON=$(cat < y\" so I guess that means that z > x ","data": "This text contains a CEND ]]>","alternative": "This text contains a CEND ]]>"}} +{"a":{"description":"An example of escaped CENDs","sometext":" They're saying \"x < y\" & that \"z > y\" so I guess that means that z > x ","data":"This text contains a CEND ]]>","alternative":"This text contains a CEND ]]>"}} EOF ) new "xml complex CDATA to json" @@ -74,7 +74,7 @@ new "xml encode <>&" expecteof "$clixon_util_xml" 0 "$XML" "$XML" new "xml encode <>& to json" -expecteof "$clixon_util_xml -j" 0 "$XML" '{"message": "Less than: < , greater than: > ampersand: & "}' +expecteof "$clixon_util_xml -j" 0 "$XML" '{"message":"Less than: < , greater than: > ampersand: & "}' XML=$(cat <single-quote character ' represented as ' and double-quote character as " @@ -84,7 +84,7 @@ new "xml single and double quote" expecteof "$clixon_util_xml" 0 "$XML" "single-quote character ' represented as ' and double-quote character as \"" JSON=$(cat <a\b" "a\b" new "xml backspace to json" -expecteofx "$clixon_util_xml -j" 0 "a\b" '{"a": "a\\b"}' +expecteofx "$clixon_util_xml -j" 0 "a\b" '{"a":"a\\b"}' new "Double quotes for attributes" expecteof "$clixon_util_xml" 0 '' '' diff --git a/test/test_yang_extension.sh b/test/test_yang_extension.sh new file mode 100755 index 00000000..87328772 --- /dev/null +++ b/test/test_yang_extension.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Yang extensions and unknown statements. +# 1) First test syntax +# Assuming the following extension definition: +# prefix p; +# extension keyw { +# argument arg; # optional +# } +# there are four forms of unknown statement as follows: +# p:keyw; +# p:keyw arg; +# p:keyw { stmt;* } +# p:keyw arg { stmt;* } +# +# 2) The extensions results in in a node data definition. +# Second, the example is run without the extension enabled, then it is enabled. + +# 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/$APPNAME.yang + +cat < $cfg + + $cfg + /usr/local/share/clixon + $dir + $IETFRFC + $fyang + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/cli + /usr/local/lib/$APPNAME/netconf + /usr/local/lib/$APPNAME/backend + $APPNAME + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + 1 + /usr/local/var/$APPNAME + true + +EOF + +cat < $fyang +module $APPNAME{ + yang-version 1.1; + prefix ex; + namespace "urn:example:clixon"; + extension e1 { + description "no argument, no statements"; + } + extension e2 { + description "with argument, no statements"; + argument arg; + } + extension e3 { + description "no argument, with statement"; + } + extension e4 { + description "with argument, with statement"; + argument arg; + } + grouping foo { + leaf foo{ + type string; + } + } + grouping bar { + leaf bar{ + type string; + } + } + + ex:e1; + ex:e2 arg1; + ex:e3 { + uses foo; + } + ex:e4 arg1{ + uses bar; + } +} +EOF + +XML='a string' + +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_backend -s init -f $cfg + + new "waiting" + wait_backend +fi + +# The main example implements ex:e4 +new "Add extension foo (not implemented)" +expecteof "$clixon_netconf -qf $cfg -D $DBG" 0 'a string]]>]]>' '^applicationunknown-elementfooerror' + +new "Add extension bar (is implemented)" +expecteof "$clixon_netconf -qf $cfg -D $DBG" 0 'a string]]>]]>' '^]]>]]>' + +new "netconf get config" +expecteof "$clixon_netconf -qf $cfg -D $DBG" 0 "]]>]]>" '^a string]]>]]>$' + +if [ $BE -ne 0 ]; then + 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 + sudo pkill -u root -f clixon_backend +fi + +rm -rf $dir diff --git a/test/test_yang_namespace.sh b/test/test_yang_namespace.sh index 113d0426..07691ebe 100755 --- a/test/test_yang_namespace.sh +++ b/test/test_yang_namespace.sh @@ -105,7 +105,7 @@ new "restconf set x in example1" expecteq "$(curl -s -X POST -d '{"example1:x":42}' http://localhost/restconf/data)" 0 '' new "restconf get config example1" -expecteq "$(curl -s -X GET http://localhost/restconf/data/example1:x)" 0 '{"example1:x": 42} +expecteq "$(curl -s -X GET http://localhost/restconf/data/example1:x)" 0 '{"example1:x":42} ' new "restconf set x in example2" @@ -113,17 +113,17 @@ expecteq "$(curl -s -X POST -d '{"example2:x":{"y":99}}' http://localhost/restco # XXX GET ../example1:x is translated to select=/x which gets both example1&2 #new "restconf get config example1" -#expecteq "$(curl -s -X GET http://localhost/restconf/data/example1:x)" 0 '{"example1:x": 42} +#expecteq "$(curl -s -X GET http://localhost/restconf/data/example1:x)" 0 '{"example1:x":42} # ' # XXX GET ../example2:x is translated to select=/x which gets both example1&2 #new "restconf get config example2" -#expecteq "$(curl -s -X GET http://localhost/restconf/data/example2:x)" 0 '{"example2:x": {"y":42}} +#expecteq "$(curl -s -X GET http://localhost/restconf/data/example2:x)" 0 '{"example2:x":{"y":42}} # ' new "restconf get config example1 and example2" ret=$(curl -s -X GET http://localhost/restconf/data) -expect='"example1:x": 42,"example2:x": {"y": 99}' +expect='"example1:x":42,"example2:x":{"y":99}' match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then err "$expect" "$ret" diff --git a/util/clixon_util_json.c b/util/clixon_util_json.c index 2ae5bed3..9b8e338b 100644 --- a/util/clixon_util_json.c +++ b/util/clixon_util_json.c @@ -76,6 +76,7 @@ usage(char *argv0) "\t-D \tDebug\n" "\t-j \t\tOutput as JSON\n" "\t-l \tLog on (s)yslog, std(e)rr, std(o)ut (stderr is default)\n" + "\t-p \t\tPretty-print output\n" "\t-y \tyang filename to parse (must be stand-alone)\n" , argv0); exit(0); @@ -96,10 +97,11 @@ main(int argc, yang_stmt *yspec = NULL; cxobj *xerr = NULL; /* malloced must be freed */ int ret; + int pretty = 0; optind = 1; opterr = 0; - while ((c = getopt(argc, argv, "hD:jl:y:")) != -1) + while ((c = getopt(argc, argv, "hD:jl:py:")) != -1) switch (c) { case 'h': usage(argv[0]); @@ -115,6 +117,9 @@ main(int argc, if ((logdst = clicon_log_opt(optarg[0])) < 0) usage(argv[0]); break; + case 'p': + pretty++; + break; case 'y': yang_filename = optarg; break; @@ -140,9 +145,9 @@ main(int argc, xc = NULL; while ((xc = xml_child_each(xt, xc, -1)) != NULL) if (json) - xml2json_cbuf(cb, xc, 0); /* print xml */ + xml2json_cbuf(cb, xc, pretty); /* print xml */ else - clicon_xml2cbuf(cb, xc, 0, 0); /* print xml */ + clicon_xml2cbuf(cb, xc, 0, pretty); /* print xml */ fprintf(stdout, "%s", cbuf_get(cb)); fflush(stdout); retval = 0; diff --git a/yang/clixon/clixon-config@2019-06-05.yang b/yang/clixon/clixon-config@2019-06-05.yang index 6a1112d8..86898ccf 100644 --- a/yang/clixon/clixon-config@2019-06-05.yang +++ b/yang/clixon/clixon-config@2019-06-05.yang @@ -523,9 +523,8 @@ module clixon-config { leaf CLICON_STREAM_DISCOVERY_RFC8040 { type boolean; default false; - description "Enable event stream discovery as described in RFC 5277 - sections 3.2. If enabled, available streams will appear - when doing netconf get or restconf GET"; + description + "Enable monitoring information for the RESTCONF protocol from RFC 8040"; } leaf CLICON_STREAM_PATH { type string; diff --git a/yang/standard/Makefile.in b/yang/standard/Makefile.in index 47061637..44909ddb 100644 --- a/yang/standard/Makefile.in +++ b/yang/standard/Makefile.in @@ -50,6 +50,7 @@ YANGSPECS += ietf-routing@2018-03-13.yang YANGSPECS += ietf-yang-library@2016-06-21.yang YANGSPECS += ietf-netconf@2011-06-01.yang YANGSPECS += ietf-netconf-acm@2018-02-14.yang +YANGSPECS += ietf-restconf@2017-01-26.yang YANGSPECS += ietf-restconf-monitoring@2017-01-26.yang YANGSPECS += ietf-netconf-monitoring@2010-10-04.yang diff --git a/yang/standard/ietf-restconf@2017-01-26.yang b/yang/standard/ietf-restconf@2017-01-26.yang new file mode 100644 index 00000000..b47455b8 --- /dev/null +++ b/yang/standard/ietf-restconf@2017-01-26.yang @@ -0,0 +1,278 @@ +module ietf-restconf { + yang-version 1.1; + namespace "urn:ietf:params:xml:ns:yang:ietf-restconf"; + prefix "rc"; + + organization + "IETF NETCONF (Network Configuration) Working Group"; + + contact + "WG Web: + WG List: + + Author: Andy Bierman + + + Author: Martin Bjorklund + + + Author: Kent Watsen + "; + + description + "This module contains conceptual YANG specifications + for basic RESTCONF media type definitions used in + RESTCONF protocol messages. + + Note that the YANG definitions within this module do not + represent configuration data of any kind. + The 'restconf-media-type' YANG extension statement + provides a normative syntax for XML and JSON + message-encoding purposes. + + Copyright (c) 2017 IETF Trust and the persons identified as + authors of the code. All rights reserved. + + Redistribution and use in source and binary forms, with or + without modification, is permitted pursuant to, and subject + to the license terms contained in, the Simplified BSD License + set forth in Section 4.c of the IETF Trust's Legal Provisions + Relating to IETF Documents + (http://trustee.ietf.org/license-info). + + This version of this YANG module is part of RFC 8040; see + the RFC itself for full legal notices."; + + revision 2017-01-26 { + description + "Initial revision."; + reference + "RFC 8040: RESTCONF Protocol."; + } + + extension yang-data { + argument name { + yin-element true; + } + description + "This extension is used to specify a YANG data template that + represents conceptual data defined in YANG. It is + intended to describe hierarchical data independent of + protocol context or specific message-encoding format. + Data definition statements within a yang-data extension + specify the generic syntax for the specific YANG data + template, whose name is the argument of the 'yang-data' + extension statement. + + Note that this extension does not define a media type. + A specification using this extension MUST specify the + message-encoding rules, including the content media type. + + The mandatory 'name' parameter value identifies the YANG + data template that is being defined. It contains the + template name. + + This extension is ignored unless it appears as a top-level + statement. It MUST contain data definition statements + that result in exactly one container data node definition. + An instance of a YANG data template can thus be translated + into an XML instance document, whose top-level element + corresponds to the top-level container. + The module name and namespace values for the YANG module using + the extension statement are assigned to instance document data + conforming to the data definition statements within + this extension. + + The substatements of this extension MUST follow the + 'data-def-stmt' rule in the YANG ABNF. + + The XPath document root is the extension statement itself, + such that the child nodes of the document root are + represented by the data-def-stmt substatements within + this extension. This conceptual document is the context + for the following YANG statements: + + - must-stmt + - when-stmt + - path-stmt + - min-elements-stmt + - max-elements-stmt + - mandatory-stmt + - unique-stmt + - ordered-by + - instance-identifier data type + + The following data-def-stmt substatements are constrained + when used within a 'yang-data' extension statement. + + - The list-stmt is not required to have a key-stmt defined. + - The if-feature-stmt is ignored if present. + - The config-stmt is ignored if present. + - The available identity values for any 'identityref' + leaf or leaf-list nodes are limited to the module + containing this extension statement and the modules + imported into that module. + "; + } + + rc:yang-data yang-errors { + uses errors; + } + + rc:yang-data yang-api { + uses restconf; + } + + grouping errors { + description + "A grouping that contains a YANG container + representing the syntax and semantics of a + YANG Patch error report within a response message."; + + container errors { + description + "Represents an error report returned by the server if + a request results in an error."; + + list error { + description + "An entry containing information about one + specific error that occurred while processing + a RESTCONF request."; + reference + "RFC 6241, Section 4.3."; + + leaf error-type { + type enumeration { + enum transport { + description + "The transport layer."; + } + enum rpc { + description + "The rpc or notification layer."; + } + enum protocol { + description + "The protocol operation layer."; + } + enum application { + description + "The server application layer."; + } + } + mandatory true; + description + "The protocol layer where the error occurred."; + } + + leaf error-tag { + type string; + mandatory true; + description + "The enumerated error-tag."; + } + + leaf error-app-tag { + type string; + description + "The application-specific error-tag."; + } + + leaf error-path { + type instance-identifier; + description + "The YANG instance identifier associated + with the error node."; + } + + leaf error-message { + type string; + description + "A message describing the error."; + } + + anydata error-info { + description + "This anydata value MUST represent a container with + zero or more data nodes representing additional + error information."; + } + } + } + } + + grouping restconf { + description + "Conceptual grouping representing the RESTCONF + root resource."; + + container restconf { + description + "Conceptual container representing the RESTCONF + root resource."; + + container data { + description + "Container representing the datastore resource. + Represents the conceptual root of all state data + and configuration data supported by the server. + The child nodes of this container can be any data + resources that are defined as top-level data nodes + from the YANG modules advertised by the server in + the 'ietf-yang-library' module."; + } + + container operations { + description + "Container for all operation resources. + + Each resource is represented as an empty leaf with the + name of the RPC operation from the YANG 'rpc' statement. + + For example, the 'system-restart' RPC operation defined + in the 'ietf-system' module would be represented as + an empty leaf in the 'ietf-system' namespace. This is + a conceptual leaf and will not actually be found in + the module: + + module ietf-system { + leaf system-reset { + type empty; + } + } + + To invoke the 'system-restart' RPC operation: + + POST /restconf/operations/ietf-system:system-restart + + To discover the RPC operations supported by the server: + + GET /restconf/operations + + In XML, the YANG module namespace identifies the module: + + + + In JSON, the YANG module name identifies the module: + + { 'ietf-system:system-restart' : [null] } + "; + } + leaf yang-library-version { + type string { + pattern '\d{4}-\d{2}-\d{2}'; + } + config false; + mandatory true; + description + "Identifies the revision date of the 'ietf-yang-library' + module that is implemented by this RESTCONF server. + Indicates the year, month, and day in YYYY-MM-DD + numeric format."; + } + } + } + +} From 291f1735055dfb4c96525c35b2759b9484ef5922 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 24 Jul 2019 09:46:08 +0200 Subject: [PATCH 02/10] libm needed in xpath --- CHANGELOG.md | 2 +- apps/cli/cli_generate.c | 1 - configure | 1197 ++++++++++++++++++++++++++--------- configure.ac | 2 + example/main/example_cli.c | 1 - include/clixon_config.h.in | 3 + lib/src/clixon_xpath.c | 1 - lib/src/clixon_xpath_ctx.c | 2 +- lib/src/clixon_xpath_eval.c | 2 +- 9 files changed, 896 insertions(+), 315 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 463ab2c6..cc712d2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ ### Minor changes * Removed unnecessary configure dependencies - * libnsl, libcrypt, libm, if_vlan,... + * libnsl, libcrypt, if_vlan,... * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs diff --git a/apps/cli/cli_generate.c b/apps/cli/cli_generate.c index 03535e27..5e3381be 100644 --- a/apps/cli/cli_generate.c +++ b/apps/cli/cli_generate.c @@ -52,7 +52,6 @@ #include #include #include -#include /* For pow() kludge in cvtype_max2str_dup2 */ /* cligen */ #include diff --git a/configure b/configure index 2a1df8d0..e0a21472 100755 --- a/configure +++ b/configure @@ -631,9 +631,6 @@ LEX YFLAGS YACC CPP -OBJEXT -EXEEXT -ac_ct_CC wwwuser wwwdir enable_stdyangs @@ -645,11 +642,7 @@ INSTALL INSTALL_DATA INSTALL_SCRIPT INSTALL_PROGRAM -CPPFLAGS INCLUDES -LDFLAGS -CFLAGS -CC target_os target_vendor target_cpu @@ -667,6 +660,13 @@ CLIXON_VERSION_MINOR CLIXON_VERSION_MAJOR CLIXON_VERSION_STRING CLIXON_VERSION +OBJEXT +EXEEXT +ac_ct_CC +CPPFLAGS +LDFLAGS +CFLAGS +CC target_alias host_alias build_alias @@ -1505,43 +1505,6 @@ fi } # ac_fn_c_try_compile -# ac_fn_c_try_cpp LINENO -# ---------------------- -# Try to preprocess conftest.$ac_ext, and return whether this succeeded. -ac_fn_c_try_cpp () -{ - as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack - if { { ac_try="$ac_cpp conftest.$ac_ext" -case "(($ac_try" in - *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; - *) ac_try_echo=$ac_try;; -esac -eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" -$as_echo "$ac_try_echo"; } >&5 - (eval "$ac_cpp conftest.$ac_ext") 2>conftest.err - ac_status=$? - if test -s conftest.err; then - grep -v '^ *+' conftest.err >conftest.er1 - cat conftest.er1 >&5 - mv -f conftest.er1 conftest.err - fi - $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 - test $ac_status = 0; } > conftest.i && { - test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || - test ! -s conftest.err - }; then : - ac_retval=0 -else - $as_echo "$as_me: failed program was:" >&5 -sed 's/^/| /' conftest.$ac_ext >&5 - - ac_retval=1 -fi - eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno - as_fn_set_status $ac_retval - -} # ac_fn_c_try_cpp - # ac_fn_c_try_link LINENO # ----------------------- # Try to link conftest.$ac_ext, and return whether this succeeded. @@ -1588,6 +1551,43 @@ fi } # ac_fn_c_try_link +# ac_fn_c_try_cpp LINENO +# ---------------------- +# Try to preprocess conftest.$ac_ext, and return whether this succeeded. +ac_fn_c_try_cpp () +{ + as_lineno=${as_lineno-"$1"} as_lineno_stack=as_lineno_stack=$as_lineno_stack + if { { ac_try="$ac_cpp conftest.$ac_ext" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_cpp conftest.$ac_ext") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + grep -v '^ *+' conftest.err >conftest.er1 + cat conftest.er1 >&5 + mv -f conftest.er1 conftest.err + fi + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } > conftest.i && { + test -z "$ac_c_preproc_warn_flag$ac_c_werror_flag" || + test ! -s conftest.err + }; then : + ac_retval=0 +else + $as_echo "$as_me: failed program was:" >&5 +sed 's/^/| /' conftest.$ac_ext >&5 + + ac_retval=1 +fi + eval $as_lineno_stack; ${as_lineno_stack:+:} unset as_lineno + as_fn_set_status $ac_retval + +} # ac_fn_c_try_cpp + # ac_fn_c_check_header_mongrel LINENO HEADER VAR INCLUDES # ------------------------------------------------------- # Tests whether HEADER exists, giving a warning if it cannot be compiled using @@ -2207,270 +2207,6 @@ cat >>confdefs.h <<_ACEOF _ACEOF - -# AC_SUBST(var) makes @var@ appear in makefiles. -# clixon versions spread to Makefile's (.so files) and variable in build.c - - - - - - -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: CLIXON version is ${CLIXON_VERSION}" >&5 -$as_echo "CLIXON version is ${CLIXON_VERSION}" >&6; } - -ac_aux_dir= -for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do - if test -f "$ac_dir/install-sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install-sh -c" - break - elif test -f "$ac_dir/install.sh"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/install.sh -c" - break - elif test -f "$ac_dir/shtool"; then - ac_aux_dir=$ac_dir - ac_install_sh="$ac_aux_dir/shtool install -c" - break - fi -done -if test -z "$ac_aux_dir"; then - as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 -fi - -# These three variables are undocumented and unsupported, -# and are intended to be withdrawn in a future Autoconf release. -# They can cause serious problems if a builder's source tree is in a directory -# whose full name contains unusual characters. -ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. -ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. -ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. - - -# Make sure we can run config.sub. -$SHELL "$ac_aux_dir/config.sub" sun4 >/dev/null 2>&1 || - as_fn_error $? "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 - -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 -$as_echo_n "checking build system type... " >&6; } -if ${ac_cv_build+:} false; then : - $as_echo_n "(cached) " >&6 -else - ac_build_alias=$build_alias -test "x$ac_build_alias" = x && - ac_build_alias=`$SHELL "$ac_aux_dir/config.guess"` -test "x$ac_build_alias" = x && - as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 -ac_cv_build=`$SHELL "$ac_aux_dir/config.sub" $ac_build_alias` || - as_fn_error $? "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 - -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 -$as_echo "$ac_cv_build" >&6; } -case $ac_cv_build in -*-*-*) ;; -*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;; -esac -build=$ac_cv_build -ac_save_IFS=$IFS; IFS='-' -set x $ac_cv_build -shift -build_cpu=$1 -build_vendor=$2 -shift; shift -# Remember, the first character of IFS is used to create $*, -# except with old shells: -build_os=$* -IFS=$ac_save_IFS -case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac - - -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking host system type" >&5 -$as_echo_n "checking host system type... " >&6; } -if ${ac_cv_host+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "x$host_alias" = x; then - ac_cv_host=$ac_cv_build -else - ac_cv_host=`$SHELL "$ac_aux_dir/config.sub" $host_alias` || - as_fn_error $? "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 -fi - -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 -$as_echo "$ac_cv_host" >&6; } -case $ac_cv_host in -*-*-*) ;; -*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;; -esac -host=$ac_cv_host -ac_save_IFS=$IFS; IFS='-' -set x $ac_cv_host -shift -host_cpu=$1 -host_vendor=$2 -shift; shift -# Remember, the first character of IFS is used to create $*, -# except with old shells: -host_os=$* -IFS=$ac_save_IFS -case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac - - -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking target system type" >&5 -$as_echo_n "checking target system type... " >&6; } -if ${ac_cv_target+:} false; then : - $as_echo_n "(cached) " >&6 -else - if test "x$target_alias" = x; then - ac_cv_target=$ac_cv_host -else - ac_cv_target=`$SHELL "$ac_aux_dir/config.sub" $target_alias` || - as_fn_error $? "$SHELL $ac_aux_dir/config.sub $target_alias failed" "$LINENO" 5 -fi - -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_target" >&5 -$as_echo "$ac_cv_target" >&6; } -case $ac_cv_target in -*-*-*) ;; -*) as_fn_error $? "invalid value of canonical target" "$LINENO" 5;; -esac -target=$ac_cv_target -ac_save_IFS=$IFS; IFS='-' -set x $ac_cv_target -shift -target_cpu=$1 -target_vendor=$2 -shift; shift -# Remember, the first character of IFS is used to create $*, -# except with old shells: -target_os=$* -IFS=$ac_save_IFS -case $target_os in *\ *) target_os=`echo "$target_os" | sed 's/ /-/g'`;; esac - - -# The aliases save the names the user supplied, while $host etc. -# will get canonicalized. -test -n "$target_alias" && - test "$program_prefix$program_suffix$program_transform_name" = \ - NONENONEs,x,x, && - program_prefix=${target_alias}- - - - - - - -# Find a good install program. We prefer a C program (faster), -# so one script is as good as another. But avoid the broken or -# incompatible versions: -# SysV /etc/install, /usr/sbin/install -# SunOS /usr/etc/install -# IRIX /sbin/install -# AIX /bin/install -# AmigaOS /C/install, which installs bootblocks on floppy discs -# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag -# AFS /usr/afsws/bin/install, which mishandles nonexistent args -# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" -# OS/2's system install, which has a completely different semantic -# ./install, which can be erroneously created by make from ./install.sh. -# Reject install programs that cannot install multiple files. -{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 -$as_echo_n "checking for a BSD-compatible install... " >&6; } -if test -z "$INSTALL"; then -if ${ac_cv_path_install+:} false; then : - $as_echo_n "(cached) " >&6 -else - as_save_IFS=$IFS; IFS=$PATH_SEPARATOR -for as_dir in $PATH -do - IFS=$as_save_IFS - test -z "$as_dir" && as_dir=. - # Account for people who put trailing slashes in PATH elements. -case $as_dir/ in #(( - ./ | .// | /[cC]/* | \ - /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ - ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ - /usr/ucb/* ) ;; - *) - # OSF1 and SCO ODT 3.0 have their own names for install. - # Don't use installbsd from OSF since it installs stuff as root - # by default. - for ac_prog in ginstall scoinst install; do - for ac_exec_ext in '' $ac_executable_extensions; do - if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then - if test $ac_prog = install && - grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then - # AIX install. It has an incompatible calling convention. - : - elif test $ac_prog = install && - grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then - # program-specific install script used by HP pwplus--don't use. - : - else - rm -rf conftest.one conftest.two conftest.dir - echo one > conftest.one - echo two > conftest.two - mkdir conftest.dir - if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && - test -s conftest.one && test -s conftest.two && - test -s conftest.dir/conftest.one && - test -s conftest.dir/conftest.two - then - ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" - break 3 - fi - fi - fi - done - done - ;; -esac - - done -IFS=$as_save_IFS - -rm -rf conftest.one conftest.two conftest.dir - -fi - if test "${ac_cv_path_install+set}" = set; then - INSTALL=$ac_cv_path_install - else - # As a last resort, use the slow shell script. Don't cache a - # value for INSTALL within a source directory, because that will - # break other packages using the cache if that directory is - # removed, or if the value is a relative name. - INSTALL=$ac_install_sh - fi -fi -{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 -$as_echo "$INSTALL" >&6; } - -# Use test -z because SunOS4 sh mishandles braces in ${var-val}. -# It thinks the first close brace ends the variable substitution. -test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' - -test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' - -test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' - - - - - - - - - # If yes, compile apps/restconf - -wwwdir=/www-data - -wwwuser=www-data - -# ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -3260,6 +2996,850 @@ ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' ac_compiler_gnu=$ac_cv_c_compiler_gnu + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for main in -lm" >&5 +$as_echo_n "checking for main in -lm... " >&6; } +if ${ac_cv_lib_m_main+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_check_lib_save_LIBS=$LIBS +LIBS="-lm $LIBS" +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + +int +main () +{ +return main (); + ; + return 0; +} +_ACEOF +if ac_fn_c_try_link "$LINENO"; then : + ac_cv_lib_m_main=yes +else + ac_cv_lib_m_main=no +fi +rm -f core conftest.err conftest.$ac_objext \ + conftest$ac_exeext conftest.$ac_ext +LIBS=$ac_check_lib_save_LIBS +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_lib_m_main" >&5 +$as_echo "$ac_cv_lib_m_main" >&6; } +if test "x$ac_cv_lib_m_main" = xyes; then : + cat >>confdefs.h <<_ACEOF +#define HAVE_LIBM 1 +_ACEOF + + LIBS="-lm $LIBS" + +fi + + +# AC_SUBST(var) makes @var@ appear in makefiles. +# clixon versions spread to Makefile's (.so files) and variable in build.c + + + + + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: CLIXON version is ${CLIXON_VERSION}" >&5 +$as_echo "CLIXON version is ${CLIXON_VERSION}" >&6; } + +ac_aux_dir= +for ac_dir in "$srcdir" "$srcdir/.." "$srcdir/../.."; do + if test -f "$ac_dir/install-sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install-sh -c" + break + elif test -f "$ac_dir/install.sh"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/install.sh -c" + break + elif test -f "$ac_dir/shtool"; then + ac_aux_dir=$ac_dir + ac_install_sh="$ac_aux_dir/shtool install -c" + break + fi +done +if test -z "$ac_aux_dir"; then + as_fn_error $? "cannot find install-sh, install.sh, or shtool in \"$srcdir\" \"$srcdir/..\" \"$srcdir/../..\"" "$LINENO" 5 +fi + +# These three variables are undocumented and unsupported, +# and are intended to be withdrawn in a future Autoconf release. +# They can cause serious problems if a builder's source tree is in a directory +# whose full name contains unusual characters. +ac_config_guess="$SHELL $ac_aux_dir/config.guess" # Please don't use this var. +ac_config_sub="$SHELL $ac_aux_dir/config.sub" # Please don't use this var. +ac_configure="$SHELL $ac_aux_dir/configure" # Please don't use this var. + + +# Make sure we can run config.sub. +$SHELL "$ac_aux_dir/config.sub" sun4 >/dev/null 2>&1 || + as_fn_error $? "cannot run $SHELL $ac_aux_dir/config.sub" "$LINENO" 5 + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking build system type" >&5 +$as_echo_n "checking build system type... " >&6; } +if ${ac_cv_build+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_build_alias=$build_alias +test "x$ac_build_alias" = x && + ac_build_alias=`$SHELL "$ac_aux_dir/config.guess"` +test "x$ac_build_alias" = x && + as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5 +ac_cv_build=`$SHELL "$ac_aux_dir/config.sub" $ac_build_alias` || + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $ac_build_alias failed" "$LINENO" 5 + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5 +$as_echo "$ac_cv_build" >&6; } +case $ac_cv_build in +*-*-*) ;; +*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;; +esac +build=$ac_cv_build +ac_save_IFS=$IFS; IFS='-' +set x $ac_cv_build +shift +build_cpu=$1 +build_vendor=$2 +shift; shift +# Remember, the first character of IFS is used to create $*, +# except with old shells: +build_os=$* +IFS=$ac_save_IFS +case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking host system type" >&5 +$as_echo_n "checking host system type... " >&6; } +if ${ac_cv_host+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test "x$host_alias" = x; then + ac_cv_host=$ac_cv_build +else + ac_cv_host=`$SHELL "$ac_aux_dir/config.sub" $host_alias` || + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $host_alias failed" "$LINENO" 5 +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5 +$as_echo "$ac_cv_host" >&6; } +case $ac_cv_host in +*-*-*) ;; +*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;; +esac +host=$ac_cv_host +ac_save_IFS=$IFS; IFS='-' +set x $ac_cv_host +shift +host_cpu=$1 +host_vendor=$2 +shift; shift +# Remember, the first character of IFS is used to create $*, +# except with old shells: +host_os=$* +IFS=$ac_save_IFS +case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac + + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking target system type" >&5 +$as_echo_n "checking target system type... " >&6; } +if ${ac_cv_target+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test "x$target_alias" = x; then + ac_cv_target=$ac_cv_host +else + ac_cv_target=`$SHELL "$ac_aux_dir/config.sub" $target_alias` || + as_fn_error $? "$SHELL $ac_aux_dir/config.sub $target_alias failed" "$LINENO" 5 +fi + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_target" >&5 +$as_echo "$ac_cv_target" >&6; } +case $ac_cv_target in +*-*-*) ;; +*) as_fn_error $? "invalid value of canonical target" "$LINENO" 5;; +esac +target=$ac_cv_target +ac_save_IFS=$IFS; IFS='-' +set x $ac_cv_target +shift +target_cpu=$1 +target_vendor=$2 +shift; shift +# Remember, the first character of IFS is used to create $*, +# except with old shells: +target_os=$* +IFS=$ac_save_IFS +case $target_os in *\ *) target_os=`echo "$target_os" | sed 's/ /-/g'`;; esac + + +# The aliases save the names the user supplied, while $host etc. +# will get canonicalized. +test -n "$target_alias" && + test "$program_prefix$program_suffix$program_transform_name" = \ + NONENONEs,x,x, && + program_prefix=${target_alias}- + + + + + + +# Find a good install program. We prefer a C program (faster), +# so one script is as good as another. But avoid the broken or +# incompatible versions: +# SysV /etc/install, /usr/sbin/install +# SunOS /usr/etc/install +# IRIX /sbin/install +# AIX /bin/install +# AmigaOS /C/install, which installs bootblocks on floppy discs +# AIX 4 /usr/bin/installbsd, which doesn't work without a -g flag +# AFS /usr/afsws/bin/install, which mishandles nonexistent args +# SVR4 /usr/ucb/install, which tries to use the nonexistent group "staff" +# OS/2's system install, which has a completely different semantic +# ./install, which can be erroneously created by make from ./install.sh. +# Reject install programs that cannot install multiple files. +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for a BSD-compatible install" >&5 +$as_echo_n "checking for a BSD-compatible install... " >&6; } +if test -z "$INSTALL"; then +if ${ac_cv_path_install+:} false; then : + $as_echo_n "(cached) " >&6 +else + as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + # Account for people who put trailing slashes in PATH elements. +case $as_dir/ in #(( + ./ | .// | /[cC]/* | \ + /etc/* | /usr/sbin/* | /usr/etc/* | /sbin/* | /usr/afsws/bin/* | \ + ?:[\\/]os2[\\/]install[\\/]* | ?:[\\/]OS2[\\/]INSTALL[\\/]* | \ + /usr/ucb/* ) ;; + *) + # OSF1 and SCO ODT 3.0 have their own names for install. + # Don't use installbsd from OSF since it installs stuff as root + # by default. + for ac_prog in ginstall scoinst install; do + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_prog$ac_exec_ext"; then + if test $ac_prog = install && + grep dspmsg "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + # AIX install. It has an incompatible calling convention. + : + elif test $ac_prog = install && + grep pwplus "$as_dir/$ac_prog$ac_exec_ext" >/dev/null 2>&1; then + # program-specific install script used by HP pwplus--don't use. + : + else + rm -rf conftest.one conftest.two conftest.dir + echo one > conftest.one + echo two > conftest.two + mkdir conftest.dir + if "$as_dir/$ac_prog$ac_exec_ext" -c conftest.one conftest.two "`pwd`/conftest.dir" && + test -s conftest.one && test -s conftest.two && + test -s conftest.dir/conftest.one && + test -s conftest.dir/conftest.two + then + ac_cv_path_install="$as_dir/$ac_prog$ac_exec_ext -c" + break 3 + fi + fi + fi + done + done + ;; +esac + + done +IFS=$as_save_IFS + +rm -rf conftest.one conftest.two conftest.dir + +fi + if test "${ac_cv_path_install+set}" = set; then + INSTALL=$ac_cv_path_install + else + # As a last resort, use the slow shell script. Don't cache a + # value for INSTALL within a source directory, because that will + # break other packages using the cache if that directory is + # removed, or if the value is a relative name. + INSTALL=$ac_install_sh + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $INSTALL" >&5 +$as_echo "$INSTALL" >&6; } + +# Use test -z because SunOS4 sh mishandles braces in ${var-val}. +# It thinks the first close brace ends the variable substitution. +test -z "$INSTALL_PROGRAM" && INSTALL_PROGRAM='${INSTALL}' + +test -z "$INSTALL_SCRIPT" && INSTALL_SCRIPT='${INSTALL}' + +test -z "$INSTALL_DATA" && INSTALL_DATA='${INSTALL} -m 644' + + + + + + + + + # If yes, compile apps/restconf + +wwwdir=/www-data + +wwwuser=www-data + + +# +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu +if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}gcc", so it can be a program name with args. +set dummy ${ac_tool_prefix}gcc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}gcc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$ac_cv_prog_CC"; then + ac_ct_CC=$CC + # Extract the first word of "gcc", so it can be a program name with args. +set dummy gcc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="gcc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +$as_echo "$ac_ct_CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +else + CC="$ac_cv_prog_CC" +fi + +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + # Extract the first word of "${ac_tool_prefix}cc", so it can be a program name with args. +set dummy ${ac_tool_prefix}cc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="${ac_tool_prefix}cc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + fi +fi +if test -z "$CC"; then + # Extract the first word of "cc", so it can be a program name with args. +set dummy cc; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else + ac_prog_rejected=no +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + if test "$as_dir/$ac_word$ac_exec_ext" = "/usr/ucb/cc"; then + ac_prog_rejected=yes + continue + fi + ac_cv_prog_CC="cc" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +if test $ac_prog_rejected = yes; then + # We found a bogon in the path, so make sure we never use it. + set dummy $ac_cv_prog_CC + shift + if test $# != 0; then + # We chose a different compiler from the bogus one. + # However, it has the same basename, so the bogon will be chosen + # first if we set CC to just the basename; use the full file name. + shift + ac_cv_prog_CC="$as_dir/$ac_word${1+' '}$@" + fi +fi +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + +fi +if test -z "$CC"; then + if test -n "$ac_tool_prefix"; then + for ac_prog in cl.exe + do + # Extract the first word of "$ac_tool_prefix$ac_prog", so it can be a program name with args. +set dummy $ac_tool_prefix$ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$CC"; then + ac_cv_prog_CC="$CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_CC="$ac_tool_prefix$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +CC=$ac_cv_prog_CC +if test -n "$CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $CC" >&5 +$as_echo "$CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$CC" && break + done +fi +if test -z "$CC"; then + ac_ct_CC=$CC + for ac_prog in cl.exe +do + # Extract the first word of "$ac_prog", so it can be a program name with args. +set dummy $ac_prog; ac_word=$2 +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $ac_word" >&5 +$as_echo_n "checking for $ac_word... " >&6; } +if ${ac_cv_prog_ac_ct_CC+:} false; then : + $as_echo_n "(cached) " >&6 +else + if test -n "$ac_ct_CC"; then + ac_cv_prog_ac_ct_CC="$ac_ct_CC" # Let the user override the test. +else +as_save_IFS=$IFS; IFS=$PATH_SEPARATOR +for as_dir in $PATH +do + IFS=$as_save_IFS + test -z "$as_dir" && as_dir=. + for ac_exec_ext in '' $ac_executable_extensions; do + if as_fn_executable_p "$as_dir/$ac_word$ac_exec_ext"; then + ac_cv_prog_ac_ct_CC="$ac_prog" + $as_echo "$as_me:${as_lineno-$LINENO}: found $as_dir/$ac_word$ac_exec_ext" >&5 + break 2 + fi +done + done +IFS=$as_save_IFS + +fi +fi +ac_ct_CC=$ac_cv_prog_ac_ct_CC +if test -n "$ac_ct_CC"; then + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_ct_CC" >&5 +$as_echo "$ac_ct_CC" >&6; } +else + { $as_echo "$as_me:${as_lineno-$LINENO}: result: no" >&5 +$as_echo "no" >&6; } +fi + + + test -n "$ac_ct_CC" && break +done + + if test "x$ac_ct_CC" = x; then + CC="" + else + case $cross_compiling:$ac_tool_warned in +yes:) +{ $as_echo "$as_me:${as_lineno-$LINENO}: WARNING: using cross tools not prefixed with host triplet" >&5 +$as_echo "$as_me: WARNING: using cross tools not prefixed with host triplet" >&2;} +ac_tool_warned=yes ;; +esac + CC=$ac_ct_CC + fi +fi + +fi + + +test -z "$CC" && { { $as_echo "$as_me:${as_lineno-$LINENO}: error: in \`$ac_pwd':" >&5 +$as_echo "$as_me: error: in \`$ac_pwd':" >&2;} +as_fn_error $? "no acceptable C compiler found in \$PATH +See \`config.log' for more details" "$LINENO" 5; } + +# Provide some information about the compiler. +$as_echo "$as_me:${as_lineno-$LINENO}: checking for C compiler version" >&5 +set X $ac_compile +ac_compiler=$2 +for ac_option in --version -v -V -qversion; do + { { ac_try="$ac_compiler $ac_option >&5" +case "(($ac_try" in + *\"* | *\`* | *\\*) ac_try_echo=\$ac_try;; + *) ac_try_echo=$ac_try;; +esac +eval ac_try_echo="\"\$as_me:${as_lineno-$LINENO}: $ac_try_echo\"" +$as_echo "$ac_try_echo"; } >&5 + (eval "$ac_compiler $ac_option >&5") 2>conftest.err + ac_status=$? + if test -s conftest.err; then + sed '10a\ +... rest of stderr output deleted ... + 10q' conftest.err >conftest.er1 + cat conftest.er1 >&5 + fi + rm -f conftest.er1 conftest.err + $as_echo "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } +done + +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether we are using the GNU C compiler" >&5 +$as_echo_n "checking whether we are using the GNU C compiler... " >&6; } +if ${ac_cv_c_compiler_gnu+:} false; then : + $as_echo_n "(cached) " >&6 +else + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ +#ifndef __GNUC__ + choke me +#endif + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_compiler_gnu=yes +else + ac_compiler_gnu=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +ac_cv_c_compiler_gnu=$ac_compiler_gnu + +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_c_compiler_gnu" >&5 +$as_echo "$ac_cv_c_compiler_gnu" >&6; } +if test $ac_compiler_gnu = yes; then + GCC=yes +else + GCC= +fi +ac_test_CFLAGS=${CFLAGS+set} +ac_save_CFLAGS=$CFLAGS +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking whether $CC accepts -g" >&5 +$as_echo_n "checking whether $CC accepts -g... " >&6; } +if ${ac_cv_prog_cc_g+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_save_c_werror_flag=$ac_c_werror_flag + ac_c_werror_flag=yes + ac_cv_prog_cc_g=no + CFLAGS="-g" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_g=yes +else + CFLAGS="" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + +else + ac_c_werror_flag=$ac_save_c_werror_flag + CFLAGS="-g" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main () +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_g=yes +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext +fi +rm -f core conftest.err conftest.$ac_objext conftest.$ac_ext + ac_c_werror_flag=$ac_save_c_werror_flag +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_g" >&5 +$as_echo "$ac_cv_prog_cc_g" >&6; } +if test "$ac_test_CFLAGS" = set; then + CFLAGS=$ac_save_CFLAGS +elif test $ac_cv_prog_cc_g = yes; then + if test "$GCC" = yes; then + CFLAGS="-g -O2" + else + CFLAGS="-g" + fi +else + if test "$GCC" = yes; then + CFLAGS="-O2" + else + CFLAGS= + fi +fi +{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for $CC option to accept ISO C89" >&5 +$as_echo_n "checking for $CC option to accept ISO C89... " >&6; } +if ${ac_cv_prog_cc_c89+:} false; then : + $as_echo_n "(cached) " >&6 +else + ac_cv_prog_cc_c89=no +ac_save_CC=$CC +cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ +#include +#include +struct stat; +/* Most of the following tests are stolen from RCS 5.7's src/conf.sh. */ +struct buf { int x; }; +FILE * (*rcsopen) (struct buf *, struct stat *, int); +static char *e (p, i) + char **p; + int i; +{ + return p[i]; +} +static char *f (char * (*g) (char **, int), char **p, ...) +{ + char *s; + va_list v; + va_start (v,p); + s = g (p, va_arg (v,int)); + va_end (v); + return s; +} + +/* OSF 4.0 Compaq cc is some sort of almost-ANSI by default. It has + function prototypes and stuff, but not '\xHH' hex character constants. + These don't provoke an error unfortunately, instead are silently treated + as 'x'. The following induces an error, until -std is added to get + proper ANSI mode. Curiously '\x00'!='x' always comes out true, for an + array size at least. It's necessary to write '\x00'==0 to get something + that's true only with -std. */ +int osf4_cc_array ['\x00' == 0 ? 1 : -1]; + +/* IBM C 6 for AIX is almost-ANSI by default, but it replaces macro parameters + inside strings and character constants. */ +#define FOO(x) 'x' +int xlc6_cc_array[FOO(a) == 'x' ? 1 : -1]; + +int test (int i, double x); +struct s1 {int (*f) (int a);}; +struct s2 {int (*f) (double a);}; +int pairnames (int, char **, FILE *(*)(struct buf *, struct stat *, int), int, int); +int argc; +char **argv; +int +main () +{ +return f (e, argv, 0) != argv[0] || f (e, argv, 1) != argv[1]; + ; + return 0; +} +_ACEOF +for ac_arg in '' -qlanglvl=extc89 -qlanglvl=ansi -std \ + -Ae "-Aa -D_HPUX_SOURCE" "-Xc -D__EXTENSIONS__" +do + CC="$ac_save_CC $ac_arg" + if ac_fn_c_try_compile "$LINENO"; then : + ac_cv_prog_cc_c89=$ac_arg +fi +rm -f core conftest.err conftest.$ac_objext + test "x$ac_cv_prog_cc_c89" != "xno" && break +done +rm -f conftest.$ac_ext +CC=$ac_save_CC + +fi +# AC_CACHE_VAL +case "x$ac_cv_prog_cc_c89" in + x) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: none needed" >&5 +$as_echo "none needed" >&6; } ;; + xno) + { $as_echo "$as_me:${as_lineno-$LINENO}: result: unsupported" >&5 +$as_echo "unsupported" >&6; } ;; + *) + CC="$CC $ac_cv_prog_cc_c89" + { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_cv_prog_cc_c89" >&5 +$as_echo "$ac_cv_prog_cc_c89" >&6; } ;; +esac +if test "x$ac_cv_prog_cc_c89" != xno; then : + +fi + +ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + ac_ext=c ac_cpp='$CPP $CPPFLAGS' ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -3474,7 +4054,6 @@ fi done test -n "$YACC" || YACC="yacc" - for ac_prog in flex lex do # Extract the first word of "$ac_prog", so it can be a program name with args. diff --git a/configure.ac b/configure.ac index 59c9658b..bdebd977 100644 --- a/configure.ac +++ b/configure.ac @@ -61,6 +61,7 @@ AC_DEFINE_UNQUOTED(CLIXON_VERSION_MAJOR, $CLIXON_VERSION_MAJOR, [Clixon major re AC_DEFINE_UNQUOTED(CLIXON_VERSION_MINOR, $CLIXON_VERSION_MINOR, [Clixon minor release]) AC_DEFINE_UNQUOTED(CLIXON_VERSION_PATCH, $CLIXON_VERSION_PATCH, [Clixon path version]) +AC_CHECK_LIB(m, main) # AC_SUBST(var) makes @var@ appear in makefiles. # clixon versions spread to Makefile's (.so files) and variable in build.c @@ -91,6 +92,7 @@ AC_SUBST(with_restconf) # If yes, compile apps/restconf AC_SUBST(enable_stdyangs) AC_SUBST(wwwdir,/www-data) AC_SUBST(wwwuser,www-data) + # AC_PROG_CC() AC_PROG_CPP diff --git a/example/main/example_cli.c b/example/main/example_cli.c index 17cda3e8..adbdefba 100644 --- a/example/main/example_cli.c +++ b/example/main/example_cli.c @@ -39,7 +39,6 @@ #include #include #include -#include #include #include #include diff --git a/include/clixon_config.h.in b/include/clixon_config.h.in index a5b70be8..08f448f2 100644 --- a/include/clixon_config.h.in +++ b/include/clixon_config.h.in @@ -42,6 +42,9 @@ /* Define to 1 if you have the `fcgi' library (-lfcgi). */ #undef HAVE_LIBFCGI +/* Define to 1 if you have the `m' library (-lm). */ +#undef HAVE_LIBM + /* Define to 1 if you have the `socket' library (-lsocket). */ #undef HAVE_LIBSOCKET diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index 4f4cd374..22bd7677 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -70,7 +70,6 @@ #include #include #include -#include /* cligen */ #include diff --git a/lib/src/clixon_xpath_ctx.c b/lib/src/clixon_xpath_ctx.c index 5915a184..fbffa5c5 100644 --- a/lib/src/clixon_xpath_ctx.c +++ b/lib/src/clixon_xpath_ctx.c @@ -45,7 +45,7 @@ #include #include #include -#include +#include /* NaN */ /* cligen */ #include diff --git a/lib/src/clixon_xpath_eval.c b/lib/src/clixon_xpath_eval.c index a7b780af..2bc21d9e 100644 --- a/lib/src/clixon_xpath_eval.c +++ b/lib/src/clixon_xpath_eval.c @@ -68,7 +68,7 @@ #include #include #include -#include +#include /* NaN */ /* cligen */ #include From c1bae276ffbcd1a8b70c27c0be5d3fdd88e520a5 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 25 Jul 2019 13:15:31 +0200 Subject: [PATCH 03/10] * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * HTTP `Location:` fields added in RESTCONF POST replies --- CHANGELOG.md | 15 +- README.md | 1 + apps/restconf/Makefile.in | 2 + apps/restconf/clixon_restconf.h | 4 +- apps/restconf/restconf_lib.c | 54 +- apps/restconf/restconf_lib.h | 5 +- apps/restconf/restconf_main.c | 8 +- apps/restconf/restconf_methods.c | 1250 ++----------------------- apps/restconf/restconf_methods.h | 27 +- apps/restconf/restconf_methods_get.c | 421 +++++++++ apps/restconf/restconf_methods_get.h | 53 ++ apps/restconf/restconf_methods_post.c | 897 ++++++++++++++++++ apps/restconf/restconf_methods_post.h | 54 ++ apps/restconf/restconf_stream.c | 5 +- apps/restconf/restconf_stream.h | 2 +- lib/clixon/clixon_xml_map.h | 1 + lib/clixon/clixon_xpath.h | 2 +- lib/clixon/clixon_xpath_ctx.h | 2 +- lib/src/clixon_xml_map.c | 173 +++- lib/src/clixon_xml_sort.c | 13 +- lib/src/clixon_xpath.c | 2 +- lib/src/clixon_xpath_ctx.c | 2 +- test/test_nacm_default.sh | 2 +- test/test_nacm_module_write.sh | 4 - test/test_restconf.sh | 2 +- test/test_restconf_jukebox.sh | 25 +- test/test_submodule.sh | 6 +- 27 files changed, 1734 insertions(+), 1298 deletions(-) create mode 100644 apps/restconf/restconf_methods_get.c create mode 100644 apps/restconf/restconf_methods_get.h create mode 100644 apps/restconf/restconf_methods_post.c create mode 100644 apps/restconf/restconf_methods_post.h diff --git a/CHANGELOG.md b/CHANGELOG.md index cc712d2d..941ffcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,23 @@ ### Major New features * Restconf RFC 8040 increased feature compliance - * Cache-Control: no-cache added in HTTP responses (RFC Section 5.5) + * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: + * `201 Created` for created resources + * `204 No Content` for replaced resources. + * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) + * Implementation detail: due to difference between RESTCONF and NETCONF semantics, a PUT first to make en internal netconf edit-config create operation; if that fails, a replace operation is tried. + * HTTP `Location:` fields added in RESTCONF POST replies + * HTTP `Cache-Control: no-cache` fields added in HTTP responses (RFC Section 5.5) * Restconf monitoring capabilities (RFC Section 9.1) -* Added support for Yang extensions + * +* Yang extensions support * New plugin callback: ca_extension * Main backend example includes example code on how to implement a Yang extension in a plugin. ### API changes on existing features (you may need to change your code) +* RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: + * `201 Created` for created resources + * `204 No Content` for replaced resources. * JSON changes * Non-pretty-print output removed all extra spaces. * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` @@ -23,6 +33,7 @@ * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs +* See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * Yang Unique statements with multiple schema identifiers did not work on some platforms due to memory error. ## 4.0.0 (13 July 2019) diff --git a/README.md b/README.md index 91c10429..9fb894e8 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ The following features are supported: - query parameters start-time and stop-time(RFC8040 section 4.9) The following features are not implemented: +- ETag/Last-Modified - PATCH - query parameters other than start/stop-time. diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 9185c60c..2e9629ac 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -76,6 +76,8 @@ APPL = clixon_restconf # Not accessible from plugin APPSRC = restconf_main.c APPSRC += restconf_methods.c +APPSRC += restconf_methods_post.c +APPSRC += restconf_methods_get.c APPSRC += restconf_stream.c APPOBJ = $(APPSRC:.c=.o) diff --git a/apps/restconf/clixon_restconf.h b/apps/restconf/clixon_restconf.h index 06c6ed86..38385d40 100644 --- a/apps/restconf/clixon_restconf.h +++ b/apps/restconf/clixon_restconf.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -51,7 +51,7 @@ int notfound(FCGX_Request *r); int conflict(FCGX_Request *r); int internal_server_error(FCGX_Request *r); int notimplemented(FCGX_Request *r); -int test(FCGX_Request *r, int dbg); +int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 05cf76e8..e4d507f3 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -31,6 +31,7 @@ ***** END LICENSE BLOCK ***** + * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https */ #include @@ -302,12 +303,12 @@ printparam(FCGX_Request *r, return 0; } -/*! +/*! Print all FCGI headers * @param[in] r Fastcgi request handle */ int -test(FCGX_Request *r, - int dbg) +restconf_test(FCGX_Request *r, + int dbg) { printparam(r, "QUERY_STRING", dbg); printparam(r, "REQUEST_METHOD", dbg); @@ -328,6 +329,7 @@ test(FCGX_Request *r, printparam(r, "SERVER_NAME", dbg); printparam(r, "HTTP_COOKIE", dbg); printparam(r, "HTTPS", dbg); + printparam(r, "HTTP_HOST", dbg); printparam(r, "HTTP_ACCEPT", dbg); printparam(r, "HTTP_CONTENT_TYPE", dbg); printparam(r, "HTTP_AUTHORIZATION", dbg); @@ -478,6 +480,50 @@ api_return_err(clicon_handle h, return retval; } +/*! Print location header from FCGI environment + * @param[in] r Fastcgi request handle + * @param[in] xobj If set (eg POST) add to api-path + * $https “on” if connection operates in SSL mode, or an empty string otherwise + * @note ports are ignored + */ +int +http_location(FCGX_Request *r, + cxobj *xobj) +{ + int retval = -1; + char *https; + char *host; + char *request_uri; + cbuf *cb = NULL; + + https = FCGX_GetParam("HTTPS", r->envp); + host = FCGX_GetParam("HTTP_HOST", r->envp); + request_uri = FCGX_GetParam("REQUEST_URI", r->envp); + if (xobj != NULL){ + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + if (xml2api_path_1(xobj, cb) < 0) + goto done; + FCGX_FPrintF(r->out, "Location: http%s://%s%s%s\r\n", + https?"s":"", + host, + request_uri, + cbuf_get(cb)); + } + else + FCGX_FPrintF(r->out, "Location: http%s://%s%s\r\n", + https?"s":"", + host, + request_uri); + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + /*! Clean and close all state of restconf process (but dont exit). * Cannot use h after this * @param[in] h Clixon handle diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 1b90ef30..7270807e 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -56,11 +56,12 @@ int conflict(FCGX_Request *r); int internal_server_error(FCGX_Request *r); int notimplemented(FCGX_Request *r); -int test(FCGX_Request *r, int dbg); +int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int pretty, int use_xml, int code); +int http_location(FCGX_Request *r, cxobj *xobj); int restconf_terminate(clicon_handle h); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index e8cbe8a2..3f042975 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -78,6 +78,8 @@ /* restconf */ #include "restconf_lib.h" #include "restconf_methods.h" +#include "restconf_methods_post.h" +#include "restconf_methods_get.h" #include "restconf_stream.h" /* Command line options to be passed to getopt(3) */ @@ -368,7 +370,7 @@ api_restconf(clicon_handle h, retval = notfound(r); goto done; } - test(r, 1); + restconf_test(r, 1); if (pn == 2){ retval = api_root(h, r); @@ -429,7 +431,7 @@ api_restconf(clicon_handle h, goto done; } else if (strcmp(method, "test") == 0) - test(r, 0); + restconf_test(r, 0); else notfound(r); ok: diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 4208eca2..7d84cf92 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -146,549 +146,6 @@ api_data_options(clicon_handle h, return 0; } - -/*! Generic GET (both HEAD and GET) - * According to restconf - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @param[in] head If 1 is HEAD, otherwise GET - * @code - * curl -G http://localhost/restconf/data/interfaces/interface=eth0 - * @endcode - * See RFC8040 Sec 4.2 and 4.3 - * XXX: cant find a way to use Accept request field to choose Content-Type - * I would like to support both xml and json. - * Request may contain - * Accept: application/yang.data+json,application/yang.data+xml - * Response contains one of: - * Content-Type: application/yang-data+xml - * Content-Type: application/yang-data+json - * NOTE: If a retrieval request for a data resource representing a YANG leaf- - * list or list object identifies more than one instance, and XML - * encoding is used in the response, then an error response containing a - * "400 Bad Request" status-line MUST be returned by the server. - * Netconf: , - */ -static int -api_data_get2(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml, - int head) -{ - int retval = -1; - cbuf *cbpath = NULL; - char *xpath = NULL; - cbuf *cbx = NULL; - yang_stmt *yspec; - cxobj *xret = NULL; - cxobj *xerr = NULL; /* malloced */ - cxobj *xe = NULL; /* not malloced */ - cxobj **xvec = NULL; - size_t xlen; - int i; - cxobj *x; - int ret; - char *namespace = NULL; - cvec *nsc = NULL; - - clicon_debug(1, "%s", __FUNCTION__); - yspec = clicon_dbspec_yang(h); - if ((cbpath = cbuf_new()) == NULL) - goto done; - cprintf(cbpath, "/"); - /* We know "data" is element pi-1 */ - if ((ret = api_path2xpath_cvv(pcvec, pi, yspec, cbpath, &namespace)) < 0) - goto done; - if (ret == 0){ - if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) - goto done; - clicon_err_reset(); - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - xpath = cbuf_get(cbpath); - clicon_debug(1, "%s path:%s", __FUNCTION__, xpath); - /* Create a namespace context for ymod as the default namespace to use with - * xpath expressions */ - if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) - goto done; - if (clicon_rpc_get(h, xpath, namespace, &xret) < 0){ - if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - if (xml_apply(xret, CX_ELMNT, xml_spec_populate, yspec) < 0) - goto done; - /* We get return via netconf which is complete tree from root - * We need to cut that tree to only the object. - */ -#if 0 /* DEBUG */ - if (debug){ - cbuf *cb = cbuf_new(); - clicon_xml2cbuf(cb, xret, 0, 0); - clicon_debug(1, "%s xret:%s", __FUNCTION__, cbuf_get(cb)); - cbuf_free(cb); - } -#endif - /* Check if error return */ - if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Normal return, no error */ - if ((cbx = cbuf_new()) == NULL) - goto done; - if (head){ - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - goto ok; - } - if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ - if (use_xml){ - if (clicon_xml2cbuf(cbx, xret, 0, pretty) < 0) /* Dont print top object? */ - goto done; - } - else{ - if (xml2json_cbuf(cbx, xret, pretty) < 0) - goto done; - } - } - else{ - if (xpath_vec_nsc(xret, nsc, "%s", &xvec, &xlen, xpath) < 0){ - if (netconf_operation_failed_xml(&xerr, "application", clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Check if not exists */ - if (xlen == 0){ - /* 4.3: If a retrieval request for a data resource represents an - instance that does not exist, then an error response containing - a "404 Not Found" status-line MUST be returned by the server. - The error-tag value "invalid-value" is used in this case. */ - if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0) - goto done; - /* override invalid-value default 400 with 404 */ - if (api_return_err(h, r, xerr, pretty, use_xml, 404) < 0) - goto done; - goto ok; - } - if (use_xml){ - for (i=0; i0 - * Out: {"example:x": {"0"}} - */ - if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) - goto done; - } - } - clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\r\n\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (nsc) - xml_nsctx_free(nsc); - if (cbx) - cbuf_free(cbx); - if (cbpath) - cbuf_free(cbpath); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (xvec) - free(xvec); - return retval; -} - -/*! REST HEAD method - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * - * The HEAD method is sent by the client to retrieve just the header fields - * that would be returned for the comparable GET method, without the - * response message-body. - * Relation to netconf: none - */ -int -api_data_head(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml) -{ - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 1); -} - -/*! REST GET method - * According to restconf - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @code - * curl -G http://localhost/restconf/data/interfaces/interface=eth0 - * @endcode - * XXX: cant find a way to use Accept request field to choose Content-Type - * I would like to support both xml and json. - * Request may contain - * Accept: application/yang.data+json,application/yang.data+xml - * Response contains one of: - * Content-Type: application/yang-data+xml - * Content-Type: application/yang-data+json - * NOTE: If a retrieval request for a data resource representing a YANG leaf- - * list or list object identifies more than one instance, and XML - * encoding is used in the response, then an error response containing a - * "400 Bad Request" status-line MUST be returned by the server. - * Netconf: , - */ -int -api_data_get(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml) -{ - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 0); -} - -/*! Generic REST POST method - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where to start pcvec - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - - * @note restconf POST is mapped to edit-config create. - * See RFC8040 Sec 4.4.1 - - POST: - target resource type is datastore --> create a top-level resource - target resource type is data resource --> create child resource - - The message-body MUST contain exactly one instance of the - expected data resource. The data model for the child tree is the - subtree, as defined by YANG for the child resource. - - If the POST method succeeds, a "201 Created" status-line is returned - and there is no response message-body. A "Location" header - identifying the child resource that was created MUST be present in - the response in this case. - - If the data resource already exists, then the POST request MUST fail - and a "409 Conflict" status-line MUST be returned. - * Netconf: (nc:operation="create") | invoke an RPC operation * @example - */ -int -api_data_post(clicon_handle h, - FCGX_Request *r, - char *api_path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - enum operation_type op = OP_CREATE; - cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ - cxobj *xdata; /* -d data (without top symbol)*/ - int i; - cbuf *cbx = NULL; - cxobj *xtop = NULL; /* top of api-path */ - cxobj *xbot = NULL; /* bottom of api-path */ - yang_stmt *ybot = NULL; /* yang of xbot */ - yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ - yang_stmt *ymoddata = NULL; /* yang module of data (-d) */ - yang_stmt *yspec; - cxobj *xa; - cxobj *xret = NULL; - cxobj *xretcom = NULL; /* return from commit */ - cxobj *xretdis = NULL; /* return from discard-changes */ - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xe; /* dont free */ - char *username; - int ret; - - clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", - __FUNCTION__, - api_path, data); - if ((yspec = clicon_dbspec_yang(h)) == NULL){ - clicon_err(OE_FATAL, 0, "No DB_SPEC"); - goto done; - } - for (i=0; i", username?username:""); - cprintf(cbx, ""); - cprintf(cbx, "none"); - if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) - goto done; - cprintf(cbx, ""); - clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) - goto done; - if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Assume this is validation failed since commit includes validate */ - cbuf_reset(cbx); - /* commit/discard should be done automaticaly by the system, therefore - * recovery user is used here (edit-config but not commit may be permitted - by NACM */ - cprintf(cbx, "", NACM_RECOVERY_USER); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) - goto done; - if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ - cbuf_reset(cbx); - cprintf(cbx, "", username?username:""); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretdis, NULL) < 0) - goto done; - /* log errors from discard, but ignore */ - if ((xpath_first(xretdis, "//rpc-error")) != NULL) - clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) /* Use original xe */ - goto done; - goto ok; - } - if (xretcom){ /* Clear: can be reused again below */ - xml_free(xretcom); - xretcom = NULL; - } - if (if_feature(yspec, "ietf-netconf", "startup")){ - /* RFC8040 Sec 1.4: - * If the NETCONF server supports :startup, the RESTCONF server MUST - * automatically update the non-volatile startup configuration - * datastore, after the "running" datastore has been altered as a - * consequence of a RESTCONF edit operation. - */ - cbuf_reset(cbx); - cprintf(cbx, "", NACM_RECOVERY_USER); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) - goto done; - /* If copy-config failed, log and ignore (already committed) */ - if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ - - clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); - } - } - FCGX_SetExitStatus(201, r->out); /* Created */ - FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (xretcom) - xml_free(xretcom); - if (xretdis) - xml_free(xretdis); - if (xtop) - xml_free(xtop); - if (xdata0) - xml_free(xdata0); - if (cbx) - cbuf_free(cbx); - return retval; -} /* api_data_post */ - - /*! Check matching keys * * Check that x1 and x2 are of type list/leaf-list and share the same key statements @@ -771,16 +228,27 @@ match_list_keys(yang_stmt *y, * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data * @note restconf PUT is mapped to edit-config replace. - * See RFC8040 Sec 4.5 + * @see RFC8040 Sec 4.5 PUT * @example curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 * PUT: - if the PUT request creates a new resource, - a "201 Created" status-line is returned. If an existing resource is - modified, a "204 No Content" status-line is returned. + A request message-body MUST be present, representing the new data resource, or the server + MUST return a "400 Bad Request" status-line. + + ...if the PUT request creates a new resource, a "201 Created" status-line is returned. + If an existing resource is modified, a "204 No Content" status-line is returned. * Netconf: (nc:operation="create/replace") + * Note RFC8040 says that if an object is created, 201 is returned, if replaced 204 + * is returned. But the restconf client does not know if it is replaced or created, + * only the server knows that. Solutions: + * 1) extend the netconf so it returns if created/replaced. But that would lead + * to extension of netconf that may hit other places. + * 2) Send a get first and see if the resource exists, and then send replace/create. + * Will always produce an extra message and the GET may potetnially waste bw. + * 3) Try to create first, if that fails (with conflict) then try replace. + * --> Best solution and applied here */ int api_data_put(clicon_handle h, @@ -795,7 +263,7 @@ api_data_put(clicon_handle h, int parse_xml) { int retval = -1; - enum operation_type op = OP_REPLACE; + enum operation_type op; int i; cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ cxobj *xdata; /* -d data (without top symbol)*/ @@ -924,10 +392,12 @@ api_data_put(clicon_handle h, } } - /* Add operation (create/replace) as attribute */ + /* Add operation create as attribute. If that fails with Conflict, then try + "replace" */ if ((xa = xml_new("operation", xdata, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + op = OP_CREATE; if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; @@ -1032,6 +502,7 @@ api_data_put(clicon_handle h, /* For internal XML protocol: add username attribute for access control */ username = clicon_username_get(h); + again: cprintf(cbx, "", username?username:""); cprintf(cbx, ""); cprintf(cbx, "none"); @@ -1042,9 +513,30 @@ api_data_put(clicon_handle h, if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + /* If the error is not data-exists, then return error now + * OR we have run again with replace + */ + if (xpath_first(xe, ".[error-tag=\"data-exists\"]") == NULL || + op == OP_REPLACE){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* If it is data-exists, then set operator to replace and try again */ + if (xret){ + xml_free(xret); + xret = NULL; + } + if ((xa = xml_find_type(xdata, NULL, "operation", CX_ATTR)) == NULL){ + clicon_err(OE_XML, ENOENT, "operation attr not found (shouldnt happen)"); goto done; - goto ok; + } + op = OP_REPLACE; + if (xml_value_set(xa, xml_operation2str(op)) < 0) + goto done; + cbuf_reset(cbx); + clicon_debug(1, "%s Failed with create, trying replace",__FUNCTION__); + goto again; } cbuf_reset(cbx); /* commit/discard should be done automaticaly by the system, therefore @@ -1089,7 +581,15 @@ api_data_put(clicon_handle h, clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } - FCGX_SetExitStatus(201, r->out); /* Created */ + /* Check if it was created, or if we tried again and replaced it */ + if (op == OP_CREATE){ + FCGX_SetExitStatus(201, r->out); /* Created */ + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); + } + else{ + FCGX_SetExitStatus(204, r->out); /* Replaced */ + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); + } FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); ok: @@ -1226,7 +726,7 @@ api_data_delete(clicon_handle h, } /* Assume this is validation failed since commit includes validate */ cbuf_reset(cbx); - /* commit/discard should be done automaticaly by the system, therefore + /* commit/discard should be done automatically by the system, therefore * recovery user is used here (edit-config but not commit may be permitted by NACM */ cprintf(cbx, "", NACM_RECOVERY_USER); @@ -1268,7 +768,8 @@ api_data_delete(clicon_handle h, clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } - FCGX_SetExitStatus(201, r->out); + FCGX_SetExitStatus(204, r->out); + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); ok: @@ -1288,642 +789,3 @@ api_data_delete(clicon_handle h, return retval; } -/*! GET restconf/operations resource - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * - * @code - * curl -G http://localhost/restconf/operations - * @endcode - * RFC8040 Sec 3.3.2: - * This optional resource is a container that provides access to the - * data-model-specific RPC operations supported by the server. The - * server MAY omit this resource if no data-model-specific RPC - * operations are advertised. - * From ietf-restconf.yang: - * In XML, the YANG module namespace identifies the module: - * - * In JSON, the YANG module name identifies the module: - * { 'ietf-system:system-restart' : [null] } - */ -int -api_operations_get(clicon_handle h, - FCGX_Request *r, - char *path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml) -{ - int retval = -1; - yang_stmt *yspec; - yang_stmt *ymod; /* yang module */ - yang_stmt *yc; - char *namespace; - cbuf *cbx = NULL; - cxobj *xt = NULL; - int i; - - clicon_debug(1, "%s", __FUNCTION__); - yspec = clicon_dbspec_yang(h); - if ((cbx = cbuf_new()) == NULL) - goto done; - if (use_xml) - cprintf(cbx, ""); - else - cprintf(cbx, "{\"operations\": {"); - ymod = NULL; - i = 0; - while ((ymod = yn_each(yspec, ymod)) != NULL) { - namespace = yang_find_mynamespace(ymod); - yc = NULL; - while ((yc = yn_each(ymod, yc)) != NULL) { - if (yang_keyword_get(yc) != Y_RPC) - continue; - if (use_xml) - cprintf(cbx, "<%s xmlns=\"%s\"/>", yang_argument_get(yc), namespace); - else{ - if (i++) - cprintf(cbx, ","); - cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); - } - } - } - if (use_xml) - cprintf(cbx, ""); - else - cprintf(cbx, "}}"); - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\r\n\r\n"); - // ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (cbx) - cbuf_free(cbx); - if (xt) - xml_free(xt); - return retval; -} - -/*! Handle input data to api_operations_post - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] data Stream input data - * @param[in] yspec Yang top-level specification - * @param[in] yrpc Yang rpc spec - * @param[in] xrpc XML pointer to rpc method - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - * @retval 1 OK - * @retval 0 Fail, Error message sent - * @retval -1 Fatal error, clicon_err called - * - * RFC8040 3.6.1 - * If the "rpc" or "action" statement has an "input" section, then - * instances of these input parameters are encoded in the module - * namespace where the "rpc" or "action" statement is defined, in an XML - * element or JSON object named "input", which is in the module - * namespace where the "rpc" or "action" statement is defined. - * (Any other input is assumed as error.) - */ -static int -api_operations_post_input(clicon_handle h, - FCGX_Request *r, - char *data, - yang_stmt *yspec, - yang_stmt *yrpc, - cxobj *xrpc, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - cxobj *xdata = NULL; - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xe; - cxobj *xinput; - cxobj *x; - cbuf *cbret = NULL; - int ret; - - clicon_debug(1, "%s %s", __FUNCTION__, data); - if ((cbret = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, 0, "cbuf_new"); - goto done; - } - /* Parse input data as json or xml into xml */ - if (parse_xml){ - if (xml_parse_string(data, yspec, &xdata) < 0){ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - } - else { /* JSON */ - if ((ret = json_parse_str(data, yspec, &xdata, &xerr)) < 0){ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - if (ret == 0){ - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - } - xml_name_set(xdata, "data"); - /* Here xdata is: - * ... - */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) - goto done; - clicon_debug(1, "%s DATA:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* Validate that exactly only tag */ - if ((xinput = xml_child_i_type(xdata, 0, CX_ELMNT)) == NULL || - strcmp(xml_name(xinput),"input") != 0 || - xml_child_nr_type(xdata, CX_ELMNT) != 1){ - - if (xml_child_nr_type(xdata, CX_ELMNT) == 0){ - if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have input statement") < 0) - goto done; - } - else - if (netconf_malformed_message_xml(&xerr, "restconf RPC has malformed input statement (multiple or not called input)") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - // clicon_debug(1, "%s input validation passed", __FUNCTION__); - /* Add all input under path */ - x = NULL; - while ((x = xml_child_i_type(xinput, 0, CX_ELMNT)) != NULL) - if (xml_addsub(xrpc, x) < 0) - goto done; - /* Here xrpc is: 42 - */ - // ok: - retval = 1; - done: - clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); - if (cbret) - cbuf_free(cbret); - if (xerr) - xml_free(xerr); - if (xdata) - xml_free(xdata); - return retval; - fail: - retval = 0; - goto done; -} - -/*! Handle output data to api_operations_post - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] xret XML reply messages from backend/handler - * @param[in] yspec Yang top-level specification - * @param[in] youtput Yang rpc output specification - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[out] xoutputp Restconf JSON/XML output - * @retval 1 OK - * @retval 0 Fail, Error message sent - * @retval -1 Fatal error, clicon_err called - * xret should like: 0 - */ -static int -api_operations_post_output(clicon_handle h, - FCGX_Request *r, - cxobj *xret, - yang_stmt *yspec, - yang_stmt *youtput, - char *namespace, - int pretty, - int use_xml, - cxobj **xoutputp) - -{ - int retval = -1; - cxobj *xoutput = NULL; - cxobj *xerr = NULL; /* assumed malloced, will be freed */ - cxobj *xe; /* just pointer */ - cxobj *xa; /* xml attribute (xmlns) */ - cxobj *x; - cxobj *xok; - int isempty; - - // clicon_debug(1, "%s", __FUNCTION__); - /* Validate that exactly only tag */ - if ((xoutput = xml_child_i_type(xret, 0, CX_ELMNT)) == NULL || - strcmp(xml_name(xoutput),"rpc-reply") != 0 || - xml_child_nr_type(xret, CX_ELMNT) != 1){ - if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have single input") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - /* xoutput should now look: 0 */ - /* 9. Translate to restconf RPC data */ - xml_name_set(xoutput, "output"); - /* xoutput should now look: 0 */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xoutput, 0, 0) < 0) - goto done; - clicon_debug(1, "%s XOUTPUT:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - - /* Sanity check of outgoing XML - * For now, skip outgoing checks. - * (1) Does not handle properly - * (2) Uncertain how validation errors should be logged/handled - */ - if (youtput!=NULL){ - xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ -#if 0 - if (xml_apply(xoutput, CX_ELMNT, xml_spec_populate, yspec) < 0) - goto done; - if ((ret = xml_yang_validate_all(xoutput, &xerr)) < 0) - goto done; - if (ret == 1 && - (ret = xml_yang_validate_add(h, xoutput, &xerr)) < 0) - goto done; - if (ret == 0){ /* validation failed */ - if ((xe = xpath_first(xerr, "rpc-reply/rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } -#endif - } - /* Special case, no yang output (single - or empty body?) - * RFC 7950 7.14.4 - * If the RPC operation invocation succeeded and no output parameters - * are returned, the contains a single element - * RFC 8040 3.6.2 - * If the "rpc" statement has no "output" section, the response message - * MUST NOT include a message-body and MUST send a "204 No Content" - * status-line instead. - */ - isempty = xml_child_nr_type(xoutput, CX_ELMNT) == 0 || - (xml_child_nr_type(xoutput, CX_ELMNT) == 1 && - (xok = xml_child_i_type(xoutput, 0, CX_ELMNT)) != NULL && - strcmp(xml_name(xok),"ok")==0); - if (isempty) { - /* Internal error - invalid output from rpc handler */ - FCGX_SetExitStatus(204, r->out); /* OK */ - FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - goto fail; - } - /* Clear namespace of parameters */ - x = NULL; - while ((x = xml_child_each(xoutput, x, CX_ELMNT)) != NULL) { - if ((xa = xml_find_type(x, NULL, "xmlns", CX_ATTR)) != NULL) - if (xml_purge(xa) < 0) - goto done; - } - /* Set namespace on output */ - if (xmlns_set(xoutput, NULL, namespace) < 0) - goto done; - *xoutputp = xoutput; - retval = 1; - done: - clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); - if (xerr) - xml_free(xerr); - return retval; - fail: - retval = 0; - goto done; -} - -/*! REST operation POST method - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where to start pcvec - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - * See RFC 8040 Sec 3.6 / 4.4.2 - * @note We map post to edit-config create. - * POST {+restconf}/operations/ - * 1. Initialize - * 2. Get rpc module and name from uri (oppath) and find yang spec - * 3. Build xml tree with user and rpc: - * 4. Parse input data (arguments): - * JSON: {"example:input":{"x":0}} - * XML: 0 - * 5. Translate input args to Netconf RPC, add to xml tree: - * 42 - * 6. Validate outgoing RPC and fill in default values - * 4299 - * 7. Send to RPC handler, either local or backend - * 8. Receive reply from local/backend handler as Netconf RPC - * 0 - * 9. Translate to restconf RPC data: - * JSON: {"example:output":{"x":0}} - * XML: 0 - * 10. Validate and send reply to originator - */ -int -api_operations_post(clicon_handle h, - FCGX_Request *r, - char *path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - int i; - char *oppath = path; - yang_stmt *yspec; - yang_stmt *youtput = NULL; - yang_stmt *yrpc = NULL; - cxobj *xret = NULL; - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xtop = NULL; /* xpath root */ - cxobj *xbot = NULL; - yang_stmt *y = NULL; - cxobj *xoutput = NULL; - cxobj *xa; - cxobj *xe; - char *username; - cbuf *cbret = NULL; - int ret = 0; - char *prefix = NULL; - char *id = NULL; - yang_stmt *ys = NULL; - char *namespace = NULL; - - clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); - /* 1. Initialize */ - if ((yspec = clicon_dbspec_yang(h)) == NULL){ - clicon_err(OE_FATAL, 0, "No DB_SPEC"); - goto done; - } - if ((cbret = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, 0, "cbuf_new"); - goto done; - } - for (i=0; i - * - * The field identifies the module name and rpc identifier - * string for the desired operation. - */ - if (nodeid_split(oppath+1, &prefix, &id) < 0) /* +1 skip / */ - goto done; - if ((ys = yang_find(yspec, Y_MODULE, prefix)) == NULL){ - if (netconf_operation_failed_xml(&xerr, "protocol", "yang module not found") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - if ((yrpc = yang_find(ys, Y_RPC, id)) == NULL){ - if (netconf_missing_element_xml(&xerr, "application", id, "RPC not defined") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* 3. Build xml tree with user and rpc: - * - */ - if ((xtop = xml_new("rpc", NULL, NULL)) == NULL) - goto done; - xbot = xtop; - /* Here xtop is: */ - if ((username = clicon_username_get(h)) != NULL){ - if ((xa = xml_new("username", xtop, NULL)) == NULL) - goto done; - xml_type_set(xa, CX_ATTR); - if (xml_value_set(xa, username) < 0) - goto done; - /* Here xtop is: */ - } - if ((ret = api_path2xml(oppath, yspec, xtop, YC_SCHEMANODE, 1, &xbot, &y)) < 0) - goto done; - if (ret == 0){ /* validation failed */ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - clicon_err_reset(); - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Here xtop is: - * xbot is - * 4. Parse input data (arguments): - * JSON: {"example:input":{"x":0}} - * XML: 0 - */ - namespace = xml_find_type_value(xbot, NULL, "xmlns", CX_ATTR); - clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); - if (data && strlen(data)){ - if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, - pretty, use_xml, parse_xml)) < 0) - goto done; - if (ret == 0) - goto ok; - } - /* Here xtop is: - 42 */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 5. Translate input args: %s", - __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* 6. Validate incoming RPC and fill in defaults */ - if (xml_spec_populate_rpc(h, xtop, yspec) < 0) /* */ - goto done; - if ((ret = xml_yang_validate_rpc(h, xtop, &xret)) < 0) - goto done; - if (ret == 0){ - if ((xe = xpath_first(xret, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto ok; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Here xtop is (default values): - * 4299 - */ -#if 0 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 6. Validate and defaults:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* 7. Send to RPC handler, either local or backend - * Note (1) xtop is xbot is - * (2) local handler wants and backend wants - */ - /* Look for local (client-side) restconf plugins. - * -1:Error, 0:OK local, 1:OK backend - */ - if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) - goto done; - if (ret > 0){ /* Handled locally */ - if (xml_parse_string(cbuf_get(cbret), NULL, &xret) < 0) - goto done; - /* Local error: return it and quit */ - if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - } - else { /* Send to backend */ - if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) - goto done; - if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - } - /* 8. Receive reply from local/backend handler as Netconf RPC - * 0 - */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 8. Receive reply:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - youtput = yang_find(yrpc, Y_OUTPUT, NULL); - if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, - pretty, use_xml, &xoutput)) < 0) - goto done; - if (ret == 0) - goto ok; - /* xoutput should now look: 0 */ - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - cbuf_reset(cbret); - if (use_xml){ - if (clicon_xml2cbuf(cbret, xoutput, 0, pretty) < 0) - goto done; - /* xoutput should now look: 0 */ - } - else{ - if (xml2json_cbuf(cbret, xoutput, pretty) < 0) - goto done; - /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ - } - FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); - FCGX_FPrintF(r->out, "\r\n\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (prefix) - free(prefix); - if (id) - free(id); - if (xtop) - xml_free(xtop); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (cbret) - cbuf_free(cbret); - return retval; -} diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 42525946..89ad6460 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -30,29 +30,18 @@ the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** - + + * Restconf method implementation */ #ifndef _RESTCONF_METHODS_H_ #define _RESTCONF_METHODS_H_ -/* - * Constants - */ - /* * Prototypes */ int api_data_options(clicon_handle h, FCGX_Request *r); -int api_data_head(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); -int api_data_get(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); -int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, - cvec *pcvec, int pi, - cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, @@ -63,14 +52,4 @@ int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path, int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi, int pretty, int use_xml); -int api_operations_get(clicon_handle h, FCGX_Request *r, - char *path, - cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml); - -int api_operations_post(clicon_handle h, FCGX_Request *r, - char *path, - cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); - #endif /* _RESTCONF_METHODS_H_ */ diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c new file mode 100644 index 00000000..c89b9ede --- /dev/null +++ b/apps/restconf/restconf_methods_get.c @@ -0,0 +1,421 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for operations get and data get and head + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml-h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_methods.h" +#include "restconf_methods_get.h" + +/*! Generic GET (both HEAD and GET) + * According to restconf + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * @param[in] head If 1 is HEAD, otherwise GET + * @code + * curl -G http://localhost/restconf/data/interfaces/interface=eth0 + * @endcode + * See RFC8040 Sec 4.2 and 4.3 + * XXX: cant find a way to use Accept request field to choose Content-Type + * I would like to support both xml and json. + * Request may contain + * Accept: application/yang.data+json,application/yang.data+xml + * Response contains one of: + * Content-Type: application/yang-data+xml + * Content-Type: application/yang-data+json + * NOTE: If a retrieval request for a data resource representing a YANG leaf- + * list or list object identifies more than one instance, and XML + * encoding is used in the response, then an error response containing a + * "400 Bad Request" status-line MUST be returned by the server. + * Netconf: , + */ +static int +api_data_get2(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml, + int head) +{ + int retval = -1; + cbuf *cbpath = NULL; + char *xpath = NULL; + cbuf *cbx = NULL; + yang_stmt *yspec; + cxobj *xret = NULL; + cxobj *xerr = NULL; /* malloced */ + cxobj *xe = NULL; /* not malloced */ + cxobj **xvec = NULL; + size_t xlen; + int i; + cxobj *x; + int ret; + char *namespace = NULL; + cvec *nsc = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + yspec = clicon_dbspec_yang(h); + if ((cbpath = cbuf_new()) == NULL) + goto done; + cprintf(cbpath, "/"); + /* We know "data" is element pi-1 */ + if ((ret = api_path2xpath_cvv(pcvec, pi, yspec, cbpath, &namespace)) < 0) + goto done; + if (ret == 0){ + if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) + goto done; + clicon_err_reset(); + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + xpath = cbuf_get(cbpath); + clicon_debug(1, "%s path:%s", __FUNCTION__, xpath); + /* Create a namespace context for ymod as the default namespace to use with + * xpath expressions */ + if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) + goto done; + if (clicon_rpc_get(h, xpath, namespace, &xret) < 0){ + if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + if (xml_apply(xret, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + /* We get return via netconf which is complete tree from root + * We need to cut that tree to only the object. + */ +#if 0 /* DEBUG */ + if (debug){ + cbuf *cb = cbuf_new(); + clicon_xml2cbuf(cb, xret, 0, 0); + clicon_debug(1, "%s xret:%s", __FUNCTION__, cbuf_get(cb)); + cbuf_free(cb); + } +#endif + /* Check if error return */ + if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Normal return, no error */ + if ((cbx = cbuf_new()) == NULL) + goto done; + if (head){ + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + goto ok; + } + if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ + if (use_xml){ + if (clicon_xml2cbuf(cbx, xret, 0, pretty) < 0) /* Dont print top object? */ + goto done; + } + else{ + if (xml2json_cbuf(cbx, xret, pretty) < 0) + goto done; + } + } + else{ + if (xpath_vec_nsc(xret, nsc, "%s", &xvec, &xlen, xpath) < 0){ + if (netconf_operation_failed_xml(&xerr, "application", clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Check if not exists */ + if (xlen == 0){ + /* 4.3: If a retrieval request for a data resource represents an + instance that does not exist, then an error response containing + a "404 Not Found" status-line MUST be returned by the server. + The error-tag value "invalid-value" is used in this case. */ + if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0) + goto done; + /* override invalid-value default 400 with 404 */ + if (api_return_err(h, r, xerr, pretty, use_xml, 404) < 0) + goto done; + goto ok; + } + if (use_xml){ + for (i=0; i0 + * Out: {"example:x": {"0"}} + */ + if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) + goto done; + } + } + clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); + FCGX_FPrintF(r->out, "\r\n\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (nsc) + xml_nsctx_free(nsc); + if (cbx) + cbuf_free(cbx); + if (cbpath) + cbuf_free(cbpath); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (xvec) + free(xvec); + return retval; +} + +/*! REST HEAD method + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * + * The HEAD method is sent by the client to retrieve just the header fields + * that would be returned for the comparable GET method, without the + * response message-body. + * Relation to netconf: none + */ +int +api_data_head(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml) +{ + return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 1); +} + +/*! REST GET method + * According to restconf + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * @code + * curl -G http://localhost/restconf/data/interfaces/interface=eth0 + * @endcode + * XXX: cant find a way to use Accept request field to choose Content-Type + * I would like to support both xml and json. + * Request may contain + * Accept: application/yang.data+json,application/yang.data+xml + * Response contains one of: + * Content-Type: application/yang-data+xml + * Content-Type: application/yang-data+json + * NOTE: If a retrieval request for a data resource representing a YANG leaf- + * list or list object identifies more than one instance, and XML + * encoding is used in the response, then an error response containing a + * "400 Bad Request" status-line MUST be returned by the server. + * Netconf: , + */ +int +api_data_get(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml) +{ + return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 0); +} + +/*! GET restconf/operations resource + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * + * @code + * curl -G http://localhost/restconf/operations + * @endcode + * RFC8040 Sec 3.3.2: + * This optional resource is a container that provides access to the + * data-model-specific RPC operations supported by the server. The + * server MAY omit this resource if no data-model-specific RPC + * operations are advertised. + * From ietf-restconf.yang: + * In XML, the YANG module namespace identifies the module: + * + * In JSON, the YANG module name identifies the module: + * { 'ietf-system:system-restart' : [null] } + */ +int +api_operations_get(clicon_handle h, + FCGX_Request *r, + char *path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml) +{ + int retval = -1; + yang_stmt *yspec; + yang_stmt *ymod; /* yang module */ + yang_stmt *yc; + char *namespace; + cbuf *cbx = NULL; + cxobj *xt = NULL; + int i; + + clicon_debug(1, "%s", __FUNCTION__); + yspec = clicon_dbspec_yang(h); + if ((cbx = cbuf_new()) == NULL) + goto done; + if (use_xml) + cprintf(cbx, ""); + else + cprintf(cbx, "{\"operations\": {"); + ymod = NULL; + i = 0; + while ((ymod = yn_each(yspec, ymod)) != NULL) { + namespace = yang_find_mynamespace(ymod); + yc = NULL; + while ((yc = yn_each(ymod, yc)) != NULL) { + if (yang_keyword_get(yc) != Y_RPC) + continue; + if (use_xml) + cprintf(cbx, "<%s xmlns=\"%s\"/>", yang_argument_get(yc), namespace); + else{ + if (i++) + cprintf(cbx, ","); + cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); + } + } + } + if (use_xml) + cprintf(cbx, ""); + else + cprintf(cbx, "}}"); + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); + FCGX_FPrintF(r->out, "\r\n\r\n"); + // ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (cbx) + cbuf_free(cbx); + if (xt) + xml_free(xt); + return retval; +} + diff --git a/apps/restconf/restconf_methods_get.h b/apps/restconf/restconf_methods_get.h new file mode 100644 index 00000000..5354d581 --- /dev/null +++ b/apps/restconf/restconf_methods_get.h @@ -0,0 +1,53 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for operations get and data get and head + */ + + +#ifndef _RESTCONF_METHODS_GET_H_ +#define _RESTCONF_METHODS_GET_H_ + +/* + * Prototypes + */ +int api_data_head(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, + cvec *qvec, int pretty, int use_xml); +int api_data_get(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, + cvec *qvec, int pretty, int use_xml); +int api_operations_get(clicon_handle h, FCGX_Request *r, + char *path, + cvec *pcvec, int pi, cvec *qvec, char *data, + int pretty, int use_xml); + +#endif /* _RESTCONF_METHODS_GET_H_ */ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c new file mode 100644 index 00000000..104b37ce --- /dev/null +++ b/apps/restconf/restconf_methods_post.c @@ -0,0 +1,897 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for post: operation(rpc) and data + * From RFC 8040 Section 4.4. POST + */ + + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml.h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_methods.h" +#include "restconf_methods_post.h" + +/*! Generic REST POST method + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + + * restconf POST is mapped to edit-config create. + * @see RFC8040 Sec 4.4.1 + + POST: + target resource type is datastore --> create a top-level resource + target resource type is data resource --> create child resource + + The message-body MUST contain exactly one instance of the + expected data resource. The data model for the child tree is the + subtree, as defined by YANG for the child resource. + + If the POST method succeeds, a "201 Created" status-line is returned + and there is no response message-body. A "Location" header + identifying the child resource that was created MUST be present in + the response in this case. + + If the data resource already exists, then the POST request MUST fail + and a "409 Conflict" status-line MUST be returned. + + * @see RFC8040 Section 4.4 + */ +int +api_data_post(clicon_handle h, + FCGX_Request *r, + char *api_path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + enum operation_type op = OP_CREATE; + cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ + cxobj *xdata; /* -d data (without top symbol)*/ + int i; + cbuf *cbx = NULL; + cxobj *xtop = NULL; /* top of api-path */ + cxobj *xbot = NULL; /* bottom of api-path */ + yang_stmt *ybot = NULL; /* yang of xbot */ + yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ + yang_stmt *ymoddata = NULL; /* yang module of data (-d) */ + yang_stmt *yspec; + cxobj *xa; + cxobj *xret = NULL; + cxobj *xretcom = NULL; /* return from commit */ + cxobj *xretdis = NULL; /* return from discard-changes */ + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; /* dont free */ + char *username; + int ret; + + clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", + __FUNCTION__, + api_path, data); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + for (i=0; i", username?username:""); + cprintf(cbx, ""); + cprintf(cbx, "none"); + if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) + goto done; + cprintf(cbx, ""); + clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) + goto done; + if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Assume this is validation failed since commit includes validate */ + cbuf_reset(cbx); + /* commit/discard should be done automaticaly by the system, therefore + * recovery user is used here (edit-config but not commit may be permitted + by NACM */ + cprintf(cbx, "", NACM_RECOVERY_USER); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) + goto done; + if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ + cbuf_reset(cbx); + cprintf(cbx, "", username?username:""); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretdis, NULL) < 0) + goto done; + /* log errors from discard, but ignore */ + if ((xpath_first(xretdis, "//rpc-error")) != NULL) + clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) /* Use original xe */ + goto done; + goto ok; + } + if (xretcom){ /* Clear: can be reused again below */ + xml_free(xretcom); + xretcom = NULL; + } + if (if_feature(yspec, "ietf-netconf", "startup")){ + /* RFC8040 Sec 1.4: + * If the NETCONF server supports :startup, the RESTCONF server MUST + * automatically update the non-volatile startup configuration + * datastore, after the "running" datastore has been altered as a + * consequence of a RESTCONF edit operation. + */ + cbuf_reset(cbx); + cprintf(cbx, "", NACM_RECOVERY_USER); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) + goto done; + /* If copy-config failed, log and ignore (already committed) */ + if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ + + clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); + } + } + FCGX_SetExitStatus(201, r->out); + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); + http_location(r, xdata); + FCGX_GetParam("HTTP_ACCEPT", r->envp); + FCGX_FPrintF(r->out, "\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (xretcom) + xml_free(xretcom); + if (xretdis) + xml_free(xretdis); + if (xtop) + xml_free(xtop); + if (xdata0) + xml_free(xdata0); + if (cbx) + cbuf_free(cbx); + return retval; +} /* api_data_post */ + +/*! Handle input data to api_operations_post + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] data Stream input data + * @param[in] yspec Yang top-level specification + * @param[in] yrpc Yang rpc spec + * @param[in] xrpc XML pointer to rpc method + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * @retval 1 OK + * @retval 0 Fail, Error message sent + * @retval -1 Fatal error, clicon_err called + * + * RFC8040 3.6.1 + * If the "rpc" or "action" statement has an "input" section, then + * instances of these input parameters are encoded in the module + * namespace where the "rpc" or "action" statement is defined, in an XML + * element or JSON object named "input", which is in the module + * namespace where the "rpc" or "action" statement is defined. + * (Any other input is assumed as error.) + */ +static int +api_operations_post_input(clicon_handle h, + FCGX_Request *r, + char *data, + yang_stmt *yspec, + yang_stmt *yrpc, + cxobj *xrpc, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + cxobj *xdata = NULL; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; + cxobj *xinput; + cxobj *x; + cbuf *cbret = NULL; + int ret; + + clicon_debug(1, "%s %s", __FUNCTION__, data); + if ((cbret = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + /* Parse input data as json or xml into xml */ + if (parse_xml){ + if (xml_parse_string(data, yspec, &xdata) < 0){ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + } + else { /* JSON */ + if ((ret = json_parse_str(data, yspec, &xdata, &xerr)) < 0){ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + if (ret == 0){ + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + } + xml_name_set(xdata, "data"); + /* Here xdata is: + * ... + */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) + goto done; + clicon_debug(1, "%s DATA:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* Validate that exactly only tag */ + if ((xinput = xml_child_i_type(xdata, 0, CX_ELMNT)) == NULL || + strcmp(xml_name(xinput),"input") != 0 || + xml_child_nr_type(xdata, CX_ELMNT) != 1){ + + if (xml_child_nr_type(xdata, CX_ELMNT) == 0){ + if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have input statement") < 0) + goto done; + } + else + if (netconf_malformed_message_xml(&xerr, "restconf RPC has malformed input statement (multiple or not called input)") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + // clicon_debug(1, "%s input validation passed", __FUNCTION__); + /* Add all input under path */ + x = NULL; + while ((x = xml_child_i_type(xinput, 0, CX_ELMNT)) != NULL) + if (xml_addsub(xrpc, x) < 0) + goto done; + /* Here xrpc is: 42 + */ + // ok: + retval = 1; + done: + clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); + if (cbret) + cbuf_free(cbret); + if (xerr) + xml_free(xerr); + if (xdata) + xml_free(xdata); + return retval; + fail: + retval = 0; + goto done; +} + +/*! Handle output data to api_operations_post + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] xret XML reply messages from backend/handler + * @param[in] yspec Yang top-level specification + * @param[in] youtput Yang rpc output specification + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[out] xoutputp Restconf JSON/XML output + * @retval 1 OK + * @retval 0 Fail, Error message sent + * @retval -1 Fatal error, clicon_err called + * xret should like: 0 + */ +static int +api_operations_post_output(clicon_handle h, + FCGX_Request *r, + cxobj *xret, + yang_stmt *yspec, + yang_stmt *youtput, + char *namespace, + int pretty, + int use_xml, + cxobj **xoutputp) + +{ + int retval = -1; + cxobj *xoutput = NULL; + cxobj *xerr = NULL; /* assumed malloced, will be freed */ + cxobj *xe; /* just pointer */ + cxobj *xa; /* xml attribute (xmlns) */ + cxobj *x; + cxobj *xok; + int isempty; + + // clicon_debug(1, "%s", __FUNCTION__); + /* Validate that exactly only tag */ + if ((xoutput = xml_child_i_type(xret, 0, CX_ELMNT)) == NULL || + strcmp(xml_name(xoutput),"rpc-reply") != 0 || + xml_child_nr_type(xret, CX_ELMNT) != 1){ + if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have single input") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + /* xoutput should now look: 0 */ + /* 9. Translate to restconf RPC data */ + xml_name_set(xoutput, "output"); + /* xoutput should now look: 0 */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xoutput, 0, 0) < 0) + goto done; + clicon_debug(1, "%s XOUTPUT:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + + /* Sanity check of outgoing XML + * For now, skip outgoing checks. + * (1) Does not handle properly + * (2) Uncertain how validation errors should be logged/handled + */ + if (youtput!=NULL){ + xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ +#if 0 + if (xml_apply(xoutput, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if ((ret = xml_yang_validate_all(xoutput, &xerr)) < 0) + goto done; + if (ret == 1 && + (ret = xml_yang_validate_add(h, xoutput, &xerr)) < 0) + goto done; + if (ret == 0){ /* validation failed */ + if ((xe = xpath_first(xerr, "rpc-reply/rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } +#endif + } + /* Special case, no yang output (single - or empty body?) + * RFC 7950 7.14.4 + * If the RPC operation invocation succeeded and no output parameters + * are returned, the contains a single element + * RFC 8040 3.6.2 + * If the "rpc" statement has no "output" section, the response message + * MUST NOT include a message-body and MUST send a "204 No Content" + * status-line instead. + */ + isempty = xml_child_nr_type(xoutput, CX_ELMNT) == 0 || + (xml_child_nr_type(xoutput, CX_ELMNT) == 1 && + (xok = xml_child_i_type(xoutput, 0, CX_ELMNT)) != NULL && + strcmp(xml_name(xok),"ok")==0); + if (isempty) { + /* Internal error - invalid output from rpc handler */ + FCGX_SetExitStatus(204, r->out); /* OK */ + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); + FCGX_FPrintF(r->out, "\r\n"); + goto fail; + } + /* Clear namespace of parameters */ + x = NULL; + while ((x = xml_child_each(xoutput, x, CX_ELMNT)) != NULL) { + if ((xa = xml_find_type(x, NULL, "xmlns", CX_ATTR)) != NULL) + if (xml_purge(xa) < 0) + goto done; + } + /* Set namespace on output */ + if (xmlns_set(xoutput, NULL, namespace) < 0) + goto done; + *xoutputp = xoutput; + retval = 1; + done: + clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); + if (xerr) + xml_free(xerr); + return retval; + fail: + retval = 0; + goto done; +} + +/*! REST operation POST method + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * See RFC 8040 Sec 3.6 / 4.4.2 + * @note We map post to edit-config create. + * POST {+restconf}/operations/ + * 1. Initialize + * 2. Get rpc module and name from uri (oppath) and find yang spec + * 3. Build xml tree with user and rpc: + * 4. Parse input data (arguments): + * JSON: {"example:input":{"x":0}} + * XML: 0 + * 5. Translate input args to Netconf RPC, add to xml tree: + * 42 + * 6. Validate outgoing RPC and fill in default values + * 4299 + * 7. Send to RPC handler, either local or backend + * 8. Receive reply from local/backend handler as Netconf RPC + * 0 + * 9. Translate to restconf RPC data: + * JSON: {"example:output":{"x":0}} + * XML: 0 + * 10. Validate and send reply to originator + */ +int +api_operations_post(clicon_handle h, + FCGX_Request *r, + char *path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + int i; + char *oppath = path; + yang_stmt *yspec; + yang_stmt *youtput = NULL; + yang_stmt *yrpc = NULL; + cxobj *xret = NULL; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xtop = NULL; /* xpath root */ + cxobj *xbot = NULL; + yang_stmt *y = NULL; + cxobj *xoutput = NULL; + cxobj *xa; + cxobj *xe; + char *username; + cbuf *cbret = NULL; + int ret = 0; + char *prefix = NULL; + char *id = NULL; + yang_stmt *ys = NULL; + char *namespace = NULL; + + clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); + /* 1. Initialize */ + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + if ((cbret = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + for (i=0; i + * + * The field identifies the module name and rpc identifier + * string for the desired operation. + */ + if (nodeid_split(oppath+1, &prefix, &id) < 0) /* +1 skip / */ + goto done; + if ((ys = yang_find(yspec, Y_MODULE, prefix)) == NULL){ + if (netconf_operation_failed_xml(&xerr, "protocol", "yang module not found") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + if ((yrpc = yang_find(ys, Y_RPC, id)) == NULL){ + if (netconf_missing_element_xml(&xerr, "application", id, "RPC not defined") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* 3. Build xml tree with user and rpc: + * + */ + if ((xtop = xml_new("rpc", NULL, NULL)) == NULL) + goto done; + xbot = xtop; + /* Here xtop is: */ + if ((username = clicon_username_get(h)) != NULL){ + if ((xa = xml_new("username", xtop, NULL)) == NULL) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_value_set(xa, username) < 0) + goto done; + /* Here xtop is: */ + } + if ((ret = api_path2xml(oppath, yspec, xtop, YC_SCHEMANODE, 1, &xbot, &y)) < 0) + goto done; + if (ret == 0){ /* validation failed */ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + clicon_err_reset(); + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Here xtop is: + * xbot is + * 4. Parse input data (arguments): + * JSON: {"example:input":{"x":0}} + * XML: 0 + */ + namespace = xml_find_type_value(xbot, NULL, "xmlns", CX_ATTR); + clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); + if (data && strlen(data)){ + if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, + pretty, use_xml, parse_xml)) < 0) + goto done; + if (ret == 0) + goto ok; + } + /* Here xtop is: + 42 */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 5. Translate input args: %s", + __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* 6. Validate incoming RPC and fill in defaults */ + if (xml_spec_populate_rpc(h, xtop, yspec) < 0) /* */ + goto done; + if ((ret = xml_yang_validate_rpc(h, xtop, &xret)) < 0) + goto done; + if (ret == 0){ + if ((xe = xpath_first(xret, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto ok; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Here xtop is (default values): + * 4299 + */ +#if 0 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 6. Validate and defaults:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* 7. Send to RPC handler, either local or backend + * Note (1) xtop is xbot is + * (2) local handler wants and backend wants + */ + /* Look for local (client-side) restconf plugins. + * -1:Error, 0:OK local, 1:OK backend + */ + if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) + goto done; + if (ret > 0){ /* Handled locally */ + if (xml_parse_string(cbuf_get(cbret), NULL, &xret) < 0) + goto done; + /* Local error: return it and quit */ + if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } + else { /* Send to backend */ + if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) + goto done; + if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } + /* 8. Receive reply from local/backend handler as Netconf RPC + * 0 + */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 8. Receive reply:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + youtput = yang_find(yrpc, Y_OUTPUT, NULL); + if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, + pretty, use_xml, &xoutput)) < 0) + goto done; + if (ret == 0) + goto ok; + /* xoutput should now look: 0 */ + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + cbuf_reset(cbret); + if (use_xml){ + if (clicon_xml2cbuf(cbret, xoutput, 0, pretty) < 0) + goto done; + /* xoutput should now look: 0 */ + } + else{ + if (xml2json_cbuf(cbret, xoutput, pretty) < 0) + goto done; + /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ + } + FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); + FCGX_FPrintF(r->out, "\r\n\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (prefix) + free(prefix); + if (id) + free(id); + if (xtop) + xml_free(xtop); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (cbret) + cbuf_free(cbret); + return retval; +} diff --git a/apps/restconf/restconf_methods_post.h b/apps/restconf/restconf_methods_post.h new file mode 100644 index 00000000..cfb5e743 --- /dev/null +++ b/apps/restconf/restconf_methods_post.h @@ -0,0 +1,54 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * + * Restconf method implementation for post: operation(rpc) and data + */ + + +#ifndef _RESTCONF_METHODS_POST_H_ +#define _RESTCONF_METHODS_POST_H_ + +/* + * Prototypes + */ +int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, + cvec *pcvec, int pi, + cvec *qvec, char *data, + int pretty, int use_xml, int parse_xml); + +int api_operations_post(clicon_handle h, FCGX_Request *r, + char *path, + cvec *pcvec, int pi, cvec *qvec, char *data, + int pretty, int use_xml, int parse_xml); + +#endif /* _RESTCONF_METHODS_POST_H_ */ diff --git a/apps/restconf/restconf_stream.c b/apps/restconf/restconf_stream.c index 5c633347..3b33d506 100644 --- a/apps/restconf/restconf_stream.c +++ b/apps/restconf/restconf_stream.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -275,6 +275,7 @@ restconf_stream(clicon_handle h, } /* Setting up stream */ FCGX_SetExitStatus(201, r->out); /* Created */ + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/event-stream\r\n"); FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Connection: keep-alive\r\n"); @@ -368,7 +369,7 @@ api_stream(clicon_handle h, path = FCGX_GetParam("DOCUMENT_URI", r->envp); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - test(r, 1); + restconf_test(r, 1); if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) goto done; /* Sanity check of path. Should be /stream/ */ diff --git a/apps/restconf/restconf_stream.h b/apps/restconf/restconf_stream.h index 0cea952f..8c600a50 100644 --- a/apps/restconf/restconf_stream.h +++ b/apps/restconf/restconf_stream.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 6730b02d..d8a9423e 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -77,6 +77,7 @@ 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 xml2api_path_1(cxobj *x, cbuf *cb); 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/clixon/clixon_xpath.h b/lib/clixon/clixon_xpath.h index ece3ecdb..c8a18b0e 100644 --- a/lib/clixon/clixon_xpath.h +++ b/lib/clixon/clixon_xpath.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/clixon/clixon_xpath_ctx.h b/lib/clixon/clixon_xpath_ctx.h index 0a1bb122..1a931da0 100644 --- a/lib/clixon/clixon_xpath_ctx.h +++ b/lib/clixon/clixon_xpath_ctx.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index c3175dd5..df56244f 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1576,22 +1576,28 @@ cvec2xml_1(cvec *cvv, } /*! Recursive help function to compute differences between two xml trees - * @param[in] x0 First XML tree - * @param[in] x1 Second XML tree - * @param[out] x0vec Pointervector to XML nodes existing in only first tree - * @param[out] x0veclen Length of first vector - * @param[out] x1vec Pointervector to XML nodes existing in only second tree - * @param[out] x1veclen Length of x1vec vector - * @param[out] changed_x0 Pointervector to XML nodes changed orig value - * @param[out] changed_x1 Pointervector to XML nodes changed wanted value + * @param[in] x0 First XML tree + * @param[in] x1 Second XML tree + * @param[out] x0vec Pointervector to XML nodes existing in only first tree + * @param[out] x0veclen Length of first vector + * @param[out] x1vec Pointervector to XML nodes existing in only second tree + * @param[out] x1veclen Length of x1vec vector + * @param[out] changed_x0 Pointervector to XML nodes changed orig value + * @param[out] changed_x1 Pointervector to XML nodes changed wanted value * @param[out] changedlen Length of changed vector * Algorithm to compare two sorted lists A, B: - * A 0 1 2 3 5 6 - * B 0 2 4 5 6 - * Let a,b be first elements of A,B respectively - * a = b : recurse; get next a,b - * a < b : add a in x0, get next a - * a > b : add b in x1, get next b + * A 0 1 2 3 5 6 + * B 0 2 4 5 6 + * Let (a, b) be first elements of (A, B) respectively(*) + * a = b : EITHER leafs: a!=b : add a in changed_x0, b in changed_x1, + * OR: Set (A,B) to children of (a,b) and call algorithm recursively + * , get next (a,b) + * a < b : add a in x0, get next a + * a > b : add b in x1, get next b + * (*) "comparing" a&b here is made by xml_cmp() which judges equality from a structural + * perspective, ie both have the same yang spec, if they are lists, they have the + * the same keys. NOT that the values are equal! + * @see xml_diff API function, this one is internal and recursive */ static int xml_diff1(yang_stmt *ys, @@ -1689,18 +1695,17 @@ xml_diff1(yang_stmt *ys, } /*! Compute differences between two xml trees - * @param[in] yspec Yang specification - * @param[in] x0 First XML tree - * @param[in] x1 Second XML tree - * @param[out] first Pointervector to XML nodes existing in only first tree - * @param[out] firstlen Length of first vector - * @param[out] second Pointervector to XML nodes existing in only second tree - * @param[out] secondlen Length of second vector - * @param[out] changed_x0 Pointervector to XML nodes changed orig value - * @param[out] changed_x1 Pointervector to XML nodes changed wanted value + * @param[in] yspec Yang specification + * @param[in] x0 First XML tree + * @param[in] x1 Second XML tree + * @param[out] first Pointervector to XML nodes existing in only first tree + * @param[out] firstlen Length of first vector + * @param[out] second Pointervector to XML nodes existing in only second tree + * @param[out] secondlen Length of second vector + * @param[out] changed_x0 Pointervector to XML nodes changed orig value + * @param[out] changed_x1 Pointervector to XML nodes changed wanted value * @param[out] changedlen Length of changed vector * All xml vectors should be freed after use. - * Bot xml trees should be freed with xml_free() */ int xml_diff(yang_stmt *yspec, @@ -1750,7 +1755,9 @@ xml_diff(yang_stmt *yspec, * @param[in] ys Yang statement * @param[in] inclkey If set include key leaf (eg last leaf d in ex) * @param[out] cb api_path_fmt, - * "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3 + * @retval 0 OK + * @retval -1 Error + * @see RFC8040 3.5.3 where "api-path" is defined as "URI-encoded path expression" */ static int yang2api_path_fmt_1(yang_stmt *ys, @@ -2819,12 +2826,14 @@ xml2xpath1(cxobj *x, cprintf(cb, "/%s", xml_name(x)); if ((y = xml_spec(x)) != NULL){ keyword = yang_keyword_get(y); - if (keyword == Y_LEAF_LIST){ + switch (keyword){ + case Y_LEAF_LIST: if ((b = xml_body(x)) != NULL) cprintf(cb, "[.=\"%s\"]", b); else cprintf(cb, "[.=\"\"]"); - } else if (keyword == Y_LIST){ + break; + case Y_LIST: cvk = yang_cvec_get(y); cvi = NULL; while ((cvi = cvec_each(cvk, cvi)) != NULL) { @@ -2836,6 +2845,9 @@ xml2xpath1(cxobj *x, b = xml_body(xb); cprintf(cb, "[%s=\"%s\"]", keyname, b?b:""); } + break; + default: + break; } } retval = 0; @@ -2866,25 +2878,6 @@ xml2xpath(cxobj *x, 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"); @@ -2899,6 +2892,94 @@ xml2xpath(cxobj *x, return retval; } +/*! Construct an api_path from an XML node (single level not recursive) + * @param[in] x XML node (need to be yang populated) + * @param[out] cb api_path, must be initialized + * @retval 0 OK + * @retval -1 Error + * @see yang2api_path_fmt + * @see xml2xpath + */ +int +xml2api_path_1(cxobj *x, + cbuf *cb) +{ + int retval = -1; + yang_stmt *y = NULL; + cvec *cvk = NULL; /* vector of index keys */ + cg_var *cvi; + enum rfc_6020 keyword; + int i; + char *keyname; + cxobj *xkey; + cxobj *xb; + char *b; + char *enc; + yang_stmt *ymod; + cxobj *xp; + + if ((y = xml_spec(x)) == NULL){ + cprintf(cb, "/%s", xml_name(x)); + goto ok; + } + ymod = ys_module(y); + xp = xml_parent(x); + if (ymod && xp && xml_spec(xp)==NULL) /* Add prefix only if root */ + cprintf(cb, "/%s:%s", yang_argument_get(ymod), xml_name(x)); + else + cprintf(cb, "/%s", xml_name(x)); + keyword = yang_keyword_get(y); + switch (keyword){ + case Y_LEAF_LIST: + b = xml_body(x); + enc = NULL; + if (uri_percent_encode(&enc, "%s", b) < 0) + goto done; + cprintf(cb, "=%s", enc?enc:""); + if (enc) + free(enc); + break; + case Y_LIST: + cvk = y->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ + if (cvec_len(cvk)) + cprintf(cb, "="); + /* Iterate over individual keys */ + cvi = NULL; + i = 0; + 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; + if (i++) + cprintf(cb, ","); + b = xml_body(xb); + enc = NULL; + if (uri_percent_encode(&enc, "%s", b) < 0) + goto done; + cprintf(cb, "%s", enc?enc:""); + if (enc) + free(enc); + } + break; + default: + break; + } +#if 0 + { /* Just for testing */ + cxobj *xc; + if ((xc = xml_child_i_type(x, 0, CX_ELMNT)) != NULL) + if (xml2api_path_1(xc, cb) < 0) + goto done; + } +#endif + ok: + retval = 0; + done: + 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_xml_sort.c b/lib/src/clixon_xml_sort.c index 35be1a14..30a1f332 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -207,6 +207,15 @@ xml_child_spec(cxobj *x, * @note empty value/NULL is smallest value * @note some error cases return as -1 (qsort cant handle errors) * @note some error cases return as -1 (qsort cant handle errors) + * + * NOTE: "comparing" x1 and x2 here judges equality from a structural (model) + * perspective, ie both have the same yang spec, if they are lists, they have the + * the same keys. NOT that the values are equal! + * In other words, if x is a leaf with the same yang spec, 1 and 2 are + * "equal". + * If x is a list element (with key "k"), + * 42foo and 42bar are equal, + * but is not equal to 71bar */ int xml_cmp(cxobj *x1, @@ -299,10 +308,10 @@ xml_cmp(cxobj *x1, else{ if (xml_cv_cache(x1b, &cv1) < 0) /* error case */ goto done; - assert(cv1); + // assert(cv1); if (xml_cv_cache(x2b, &cv2) < 0) /* error case */ goto done; - assert(cv2); + // assert(cv2); if ((equal = cv_cmp(cv1, cv2)) != 0) goto done; } diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index 22bd7677..7d62b39e 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/src/clixon_xpath_ctx.c b/lib/src/clixon_xpath_ctx.c index fbffa5c5..30f44af6 100644 --- a/lib/src/clixon_xpath_ctx.c +++ b/lib/src/clixon_xpath_ctx.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index 8da722c4..c7ca696e 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Basic NACM default rulw without any groups +# Basic NACM default rule without any groups # Start from startup db # Magic line must be first in script (see README.md) diff --git a/test/test_nacm_module_write.sh b/test/test_nacm_module_write.sh index 8673b2ff..41dc1049 100755 --- a/test/test_nacm_module_write.sh +++ b/test/test_nacm_module_write.sh @@ -176,10 +176,6 @@ nacm # update | p/d | xp/dx | p/d # delete | p/d | xp/dx | p/d -#----------root -new "update root list default deny" -expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d '42$RULES')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' - # replace all, then must include NACM rules as well MSG="$RULES" new "update root list permit" diff --git a/test/test_restconf.sh b/test/test_restconf.sh index fd4aecbd..4f0c6b36 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -160,7 +160,7 @@ expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" -expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created' new "restconf Re-add subtree eth/0/0 which should give error" expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 9cabb0b2..18703884 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -7,7 +7,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi APPNAME=example cfg=$dir/conf.xml -fyang=$dir/restconf.yang +fyang=$dir/example-jukebox.yang fxml=$dir/initial.xml # example @@ -326,9 +326,28 @@ expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://l new "B.1.3. Retrieve the Server Capability Information" expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit' +new "B.2.1. Create New Data Resources (artist+json)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters" + +new "B.2.1. Create New Data Resources (album+xml)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light" + +new "B.2.1. Add Data Resources again (conflict - not in RFC)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 409 Conflict" + +new "4.5. PUT replace content" +# XXX should be: jbox:alternative --> example-jukebox:alternative +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"jbox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" + +new "4.5. PUT replace content (xml encoding)" +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" + +new "4.5. PUT create new" +# XXX should be: jbox:alternative --> example-jukebox:alternative +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" + + if false; then # NYI -new "B.2.1. Create New Data Resources" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location:" # etc new "B.2.2. Detect Datastore Resource Entity-Tag Change" new "B.2.3. Edit a Datastore Resource" diff --git a/test/test_submodule.sh b/test/test_submodule.sh index d64812b3..02dd3923 100755 --- a/test/test_submodule.sh +++ b/test/test_submodule.sh @@ -204,13 +204,13 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^< # Now same with restconf new "restconf edit main" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:main":{"x":"foo","ext":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:main":{"x":"foo","ext":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf edit sub1" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub1":{"x":"foo","ext1":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub1":{"x":"foo","ext1":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf edit sub2" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf check main/sub1/sub2 contents" expectfn "curl -s -X GET http://localhost/restconf/data" 0 '{"data":{"main:main":{"ext":"foo","x":"foo"},"main:sub1":{"ext1":"foo","x":"foo"},"main:sub2":{"ext2":"foo","x":"foo"}}}' From 5f7d011654a88adce9ed78b94d8d05871b59df6c Mon Sep 17 00:00:00 2001 From: Olof Hagsand Date: Thu, 25 Jul 2019 11:39:17 +0000 Subject: [PATCH 04/10] memleaks --- CHANGELOG.md | 2 +- apps/restconf/restconf_main.c | 7 +++++-- example/main/README.md | 22 ++++++++++----------- example/main/clixon-example@2019-07-23.yang | 6 +++--- example/main/example_backend.c | 7 +++++-- lib/src/clixon_datastore_write.c | 10 +++++++++- lib/src/clixon_yang.c | 14 ++++++++----- lib/src/clixon_yang_parse.y | 17 ++++++++++++++-- test/test_stream.sh | 8 ++++++++ 9 files changed, 66 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 463ab2c6..fb84956b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ * Restconf monitoring capabilities (RFC Section 9.1) * Added support for Yang extensions * New plugin callback: ca_extension - * Main backend example includes example code on how to implement a Yang extension in a plugin. + * The main example explains how to implement a Yang extension in a backend plugin. ### API changes on existing features (you may need to change your code) * JSON changes diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index e8cbe8a2..40f5b420 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -495,6 +495,7 @@ restconf_main_extension_cb(clicon_handle h, char *modname; yang_stmt *ymod; yang_stmt *yc; + yang_stmt *yn = NULL; ymod = ys_module(yext); modname = yang_argument_get(ymod); @@ -502,9 +503,11 @@ restconf_main_extension_cb(clicon_handle h, if (strcmp(modname, "ietf-restconf") != 0 || strcmp(extname, "yang-data") != 0) goto ok; clicon_debug(1, "%s Enabled extension:%s:%s", __FUNCTION__, modname, extname); - if ((yc = ys_prune(ys, 0)) == NULL) + if ((yc = yang_find(ys, 0, NULL)) == NULL) + goto ok; + if ((yn = ys_dup(yc)) == NULL) goto done; - if (yn_insert(yang_parent_get(ys), yc) < 0) + if (yn_insert(yang_parent_get(ys), yn) < 0) goto done; ok: retval = 0; diff --git a/example/main/README.md b/example/main/README.md index 441e5aee..baa4339d 100644 --- a/example/main/README.md +++ b/example/main/README.md @@ -183,11 +183,11 @@ Restconf support is also supported, see (restc)[../../apps/restconf/README.md]. ## RPC Operations -Clixon implements Yang RPC operations by an extension mechanism. The -extension mechanism enables you to add application-specific -operations. It works by adding user-defined callbacks for added -netconf operations. It is possible to use the extension mechanism -independent of the yang rpc construct, but it is recommended. The example includes an example: +Clixon implements Yang RPC operations by a mechanism that enables you +to add application-specific operations. It works by adding +user-defined callbacks for added netconf operations. It is possible to +use the extension mechanism independent of the yang rpc construct, but +not recommended . The example includes an example: Example using CLI: ``` @@ -273,14 +273,14 @@ The example contains some stubs for authorization according to [RFC8341(NACM)](h ## Extensions -Clixon supports Yang extensions, but you need to write plugin code. +Clixon supports Yang extensions by writing plugin callback code. The example backend implements an "example:e4" Yang extension, as follows: ``` extension e4 { description - "The first child of the ex:e4 (unknown) statement is replaced with its first - child. This means that 'uses bar;' in the ex:e4 statement below is a valid - data node"; + "The first child of the ex:e4 (unknown) statement is inserted into + the module as a regular data statement. This means that 'uses bar;' + in the ex:e4 statement below is a valid data node"; argument arg; } ex:e4 arg1{ @@ -295,8 +295,8 @@ The backend plugin code registers an extension callback in the init struct: The callback then receives a callback on all "unknown" Yang statements during yang parsing. If the extension matches "example:e4", it applies -the extension. In the example, it replaces the "ex:e4" statements with -its first child, making it a proper yang statement. +the extension. In the example, it copies the child of the "ex:e4" statement and +inserts in as a proper yang statement in the example module. ## Systemd diff --git a/example/main/clixon-example@2019-07-23.yang b/example/main/clixon-example@2019-07-23.yang index cfb4c629..da93468f 100644 --- a/example/main/clixon-example@2019-07-23.yang +++ b/example/main/clixon-example@2019-07-23.yang @@ -45,9 +45,9 @@ module clixon-example { /* yang extension implemented by the example backend code. */ extension e4 { description - "The first child of the ex:e4 (unknown) statement is replaced with its first - child. This means that 'uses bar;' in the ex:e4 statement below is a valid - data node"; + "The first child of the ex:e4 (unknown) statement is inserted into + the module as a regular data statement. This means that 'uses bar;' + in the ex:e4 statement below is a valid data node"; argument arg; } grouping bar { diff --git a/example/main/example_backend.c b/example/main/example_backend.c index 5a4f3b29..eac3e9da 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -369,6 +369,7 @@ example_extension(clicon_handle h, char *modname; yang_stmt *ymod; yang_stmt *yc; + yang_stmt *yn = NULL; ymod = ys_module(yext); modname = yang_argument_get(ymod); @@ -376,9 +377,11 @@ example_extension(clicon_handle h, if (strcmp(modname, "example") != 0 || strcmp(extname, "e4") != 0) goto ok; clicon_debug(1, "%s Enabled extension:%s:%s", __FUNCTION__, modname, extname); - if ((yc = ys_prune(ys, 0)) == NULL) + if ((yc = yang_find(ys, 0, NULL)) == NULL) + goto ok; + if ((yn = ys_dup(yc)) == NULL) goto done; - if (yn_insert(yang_parent_get(ys), yc) < 0) + if (yn_insert(yang_parent_get(ys), yn) < 0) goto done; ok: retval = 0; diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index 9689ed4e..56fd444a 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -665,6 +665,7 @@ xmldb_put(clicon_handle h, int permit = 0; /* nacm permit all */ char *format; cvec *nsc = NULL; /* nacm namespace context */ + int firsttime = 0; if (cbret == NULL){ clicon_err(OE_XML, EINVAL, "cbret is NULL"); @@ -686,6 +687,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 (xmldb_readfile(h, db, yspec, &x0, NULL) < 0) goto done; } @@ -725,8 +727,14 @@ xmldb_put(clicon_handle h, if ((ret = text_modify_top(h, x0, x1, yspec, op, username, xnacm, permit, cbret)) < 0) goto done; /* If xml return - ie netconf error xml tree, then stop and return OK */ - if (ret == 0) + if (ret == 0){ + /* If first time and quit here, x0 is not written back into cache and leaks */ + if (firsttime && x0){ + xml_free(x0); + x0 = NULL; + } goto fail; + } /* Remove NONE nodes if all subs recursively are also NONE */ if (xml_tree_prune_flagged_sub(x0, XML_FLAG_NONE, 0, NULL) <0) diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 90abe7bf..7496f465 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -2589,6 +2589,10 @@ ys_schemanode_check(yang_stmt *ys, yang_stmt *yp; char *arg; enum rfc_6020 keyword; + char **vec = NULL; + char *v; + int nvec; + int i; yp = yang_parent_get(ys); arg = yang_argument_get(ys); @@ -2609,10 +2613,6 @@ ys_schemanode_check(yang_stmt *ys, } break; case Y_UNIQUE:{ - char **vec = NULL; - char *v; - int nvec; - int i; /* Unique: Sec 7.8.3 It takes as an argument a string that contains a space- separated list of schema node identifiers */ if ((vec = clicon_strsep(arg, " \t\n", &nvec)) == NULL) @@ -2643,6 +2643,8 @@ ys_schemanode_check(yang_stmt *ys, } retval = 0; done: + if (vec) + free(vec); return retval; } @@ -3395,7 +3397,7 @@ ys_parse(yang_stmt *ys, * That is, siblings, etc, may not be there. Complete checks are made in * ys_populate instead. * @param[in] ys yang statement - * @param[in] extra Yang extra for cornercases (unknown/extension). + * @param[in] extra Yang extra for cornercases (unknown/extension). Is consumed * * The cv:s created in parse-tree as follows: * fraction-digits : Create cv as uint8, check limits [1:8] (must be made in 1st pass) @@ -3499,6 +3501,8 @@ ys_parse_sub(yang_stmt *ys, } retval = 0; done: + if (extra) + free(extra); return retval; } diff --git a/lib/src/clixon_yang_parse.y b/lib/src/clixon_yang_parse.y index 84c48830..81189eb8 100644 --- a/lib/src/clixon_yang_parse.y +++ b/lib/src/clixon_yang_parse.y @@ -231,12 +231,15 @@ yang_parse_exit(struct clicon_yang_yacc_arg *yy) return 0; } +/*! Pop a yang parse context on stack + * @param[in] yy Yang yacc argument + */ int ystack_pop(struct clicon_yang_yacc_arg *yy) { struct ys_stack *ystack; - if ((ystack = yy->yy_stack) == NULL){ + if ((ystack = yy->yy_stack) == NULL){ clicon_err(OE_YANG, 0, "ystack is NULL"); return -1; } @@ -245,6 +248,10 @@ ystack_pop(struct clicon_yang_yacc_arg *yy) return 0; } +/*! Push a yang parse context on stack + * @param[in] yy Yang yacc argument + * @param[in] yn Yang node to push + */ struct ys_stack * ystack_push(struct clicon_yang_yacc_arg *yy, yang_stmt *yn) @@ -307,7 +314,13 @@ ysp_add(struct clicon_yang_yacc_arg *yy, return NULL; } -/*! combination of ysp_add and ysp_push for sub-modules */ +/*! Add a yang statement to existing top-of-stack and then push it on stack + * + * @param[in] yy Yang yacc argument + * @param[in] keyword Yang keyword + * @param[in] argument Yang argument + * @param[in] extra Yang extra for cornercases (unknown/extension) + */ static yang_stmt * ysp_add_push(struct clicon_yang_yacc_arg *yy, enum rfc_6020 keyword, diff --git a/test/test_stream.sh b/test/test_stream.sh index a4dcd703..70713344 100755 --- a/test/test_stream.sh +++ b/test/test_stream.sh @@ -206,6 +206,8 @@ if [ $nr -lt 1 -o $nr -gt 2 ]; then err 1 "$nr" fi +sleep 2 + # 2c new "2c) start sub 8s - replay from start -8s - expect 3-4 notifications" ret=$($clixon_util_stream -u http://localhost/streams/EXAMPLE -t 10 -s -8) @@ -219,6 +221,8 @@ if [ $nr -lt 3 -o $nr -gt 4 ]; then err 4 "$nr" fi +sleep 2 + # 2d) start sub 8s - replay from start -8s to stop +4s - expect 3 notifications new "2d) start sub 8s - replay from start -8s to stop +4s - expect 3 notifications" ret=$($clixon_util_stream -u http://localhost/streams/EXAMPLE -t 10 -s -30 -e +4) @@ -232,6 +236,8 @@ if [ $nr -lt 4 -o $nr -gt 10 ]; then err 6 "$nr" fi +sleep 2 + # 2e) start sub 8s - replay from -90s w retention 60s - expect 10 notifications new "2e) start sub 8s - replay from -90s w retention 60s - expect 10 notifications" ret=$($clixon_util_stream -u http://localhost/streams/EXAMPLE -t 10 -s -90 -e +0) @@ -246,6 +252,8 @@ if [ $nr -lt 9 -o $nr -gt 14 ]; then err 10 "$nr" fi +sleep 2 + # Try parallell # start background job curl -s -X GET -H "Accept: text/event-stream" -H "Cache-Control: no-cache" -H "Connection: keep-alive" "http://localhost/streams/EXAMPLE" > /dev/null & From 70ebfa4d80a4d3958cda3110c0aaed2fd658bd77 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 28 Jul 2019 18:09:55 +0200 Subject: [PATCH 05/10] * Identity/identityref mapped between XML and JSON --- CHANGELOG.md | 5 +- apps/restconf/Makefile.in | 1 + apps/restconf/restconf_main.c | 3 +- apps/restconf/restconf_methods.c | 47 ++-- apps/restconf/restconf_methods.h | 3 - apps/restconf/restconf_methods_get.c | 12 +- apps/restconf/restconf_methods_patch.c | 151 +++++++++++ apps/restconf/restconf_methods_patch.h | 48 ++++ apps/restconf/restconf_methods_post.c | 32 ++- include/clixon_custom.h | 2 +- lib/clixon/clixon_json.h | 1 + lib/clixon/clixon_yang.h | 2 + lib/src/clixon_json.c | 362 ++++++++++++++++++++++--- lib/src/clixon_xml.c | 10 +- lib/src/clixon_xml_map.c | 42 +-- lib/src/clixon_yang.c | 95 ++++++- test/test_json.sh | 54 +++- test/test_perf_state.sh | 4 +- test/test_restconf.sh | 29 +- test/test_restconf_jukebox.sh | 5 +- test/test_xml.sh | 2 + util/clixon_util_json.c | 2 +- 22 files changed, 779 insertions(+), 133 deletions(-) create mode 100644 apps/restconf/restconf_methods_patch.c create mode 100644 apps/restconf/restconf_methods_patch.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a8623c..06601867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. + * identity/identityref mapped between XML and JSON + * XML uses prefixes, JSON uses module-names (previously prefixes were used in both cases) * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * Implementation detail: due to difference between RESTCONF and NETCONF semantics, a PUT first to make en internal netconf edit-config create operation; if that fails, a replace operation is tried. * HTTP `Location:` fields added in RESTCONF POST replies * HTTP `Cache-Control: no-cache` fields added in HTTP responses (RFC Section 5.5) * Restconf monitoring capabilities (RFC Section 9.1) - * * Yang extensions support * New plugin callback: ca_extension * The main example explains how to implement a Yang extension in a backend plugin. @@ -21,6 +22,8 @@ * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. +* RESTCONF identities has been changed to use module names instead of prefixes. + * Eg, `curl -X POST -d '{"type":"ex:eth"}` --> `curl -X POST -d '{"type":"ietf-interfaces:eth"`} * JSON changes * Non-pretty-print output removed all extra spaces. * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 2e9629ac..b2936775 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -78,6 +78,7 @@ APPSRC = restconf_main.c APPSRC += restconf_methods.c APPSRC += restconf_methods_post.c APPSRC += restconf_methods_get.c +APPSRC += restconf_methods_patch.c APPSRC += restconf_stream.c APPOBJ = $(APPSRC:.c=.o) diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index a1fb40a0..5a7da6b7 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -78,8 +78,9 @@ /* restconf */ #include "restconf_lib.h" #include "restconf_methods.h" -#include "restconf_methods_post.h" #include "restconf_methods_get.h" +#include "restconf_methods_post.h" +#include "restconf_methods_patch.h" #include "restconf_stream.h" /* Command line options to be passed to getopt(3) */ diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 7d84cf92..6cc87ce2 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -288,7 +288,7 @@ api_data_put(clicon_handle h, char *namespace0; char *dname; - clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", + clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"", __FUNCTION__, api_path0, data); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); @@ -336,6 +336,12 @@ api_data_put(clicon_handle h, } } else{ + /* Data here cannot cannot be Yang populated since it is loosely + * hanging without top symbols. + * And if it is not yang populated, it cant be translated properly + * from JSON to XML. + * Therefore, yang population is done later after addsub below + */ if ((ret = json_parse_str(data, yspec, &xdata0, &xerr)) < 0){ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; @@ -487,6 +493,21 @@ api_data_put(clicon_handle h, xml_purge(xbot); if (xml_addsub(xparent, xdata) < 0) goto done; + /* xbot is already populated, resolve yang for added xdata too */ + if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if (!parse_xml){ + /* json2xml decode could not be done above in json_parse, + need to be done here instead */ + if ((ret = json2xml_decode(xdata, &xerr)) < 0) + goto done; + if (ret == 0){ + if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } + /* If we already have that default namespace, remove it in child */ if ((xa = xml_find_type(xdata, NULL, "xmlns", CX_ATTR)) != NULL){ if (xml2ns(xparent, NULL, &namespace0) < 0) @@ -495,6 +516,7 @@ api_data_put(clicon_handle h, if (strcmp(namespace0, xml_value(xa))==0) xml_purge(xa); } + } /* Create text buffer for transfer to backend */ if ((cbx = cbuf_new()) == NULL) @@ -613,29 +635,6 @@ api_data_put(clicon_handle h, return retval; } /* api_data_put */ -/*! Generic REST PATCH method - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where to start pcvec - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * Netconf: (nc:operation="merge") - * See RFC8040 Sec 4.6 - */ -int -api_data_patch(clicon_handle h, - FCGX_Request *r, - char *api_path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data) -{ - notimplemented(r); - return 0; -} /*! Generic REST DELETE method translated to edit-config * @param[in] h CLIXON handle diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 89ad6460..55aa17f0 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -46,9 +46,6 @@ int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, int pretty, int use_xml, int parse_xml); -int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path, - cvec *pcvec, int pi, - cvec *qvec, char *data); int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi, int pretty, int use_xml); diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index c89b9ede..1fb8ad10 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -60,7 +60,6 @@ #include /* Need to be after clixon_xml-h due to attribute format */ #include "restconf_lib.h" -#include "restconf_methods.h" #include "restconf_methods_get.h" /*! Generic GET (both HEAD and GET) @@ -187,6 +186,17 @@ api_data_get2(clicon_handle h, goto done; } else{ +#if 0 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) + goto done; + clicon_debug(1, "%s xret: %s", + __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + if (xml2json_cbuf(cbx, xret, pretty) < 0) goto done; } diff --git a/apps/restconf/restconf_methods_patch.c b/apps/restconf/restconf_methods_patch.c new file mode 100644 index 00000000..39f72028 --- /dev/null +++ b/apps/restconf/restconf_methods_patch.c @@ -0,0 +1,151 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + */ + +/* + * See rfc8040 + + * sudo apt-get install libfcgi-dev + * gcc -o fastcgi fastcgi.c -lfcgi + + * sudo su -c "/www-data/clixon_restconf -D 1 f /usr/local/etc/example.xml " -s /bin/sh www-data + + * This is the interface: + * api/data/profile=/metric= PUT data:enable= + * api/test + +----------------------------+--------------------------------------+ + | 100 Continue | POST accepted, 201 should follow | + | 200 OK | Success with response message-body | + | 201 Created | POST to create a resource success | + | 204 No Content | Success without response message- | + | | body | + | 304 Not Modified | Conditional operation not done | + | 400 Bad Request | Invalid request message | + | 401 Unauthorized | Client cannot be authenticated | + | 403 Forbidden | Access to resource denied | + | 404 Not Found | Resource target or resource node not | + | | found | + | 405 Method Not Allowed | Method not allowed for target | + | | resource | + | 409 Conflict | Resource or lock in use | + | 412 Precondition Failed | Conditional method is false | + | 413 Request Entity Too | too-big error | + | Large | | + | 414 Request-URI Too Large | too-big error | + | 415 Unsupported Media Type | non RESTCONF media type | + | 500 Internal Server Error | operation-failed | + | 501 Not Implemented | unknown-operation | + | 503 Service Unavailable | Recoverable server error | + +----------------------------+--------------------------------------+ +Mapping netconf error-tag -> status code + +-------------------------+-------------+ + | | status code | + +-------------------------+-------------+ + | in-use | 409 | + | invalid-value | 400 | + | too-big | 413 | + | missing-attribute | 400 | + | bad-attribute | 400 | + | unknown-attribute | 400 | + | bad-element | 400 | + | unknown-element | 400 | + | unknown-namespace | 400 | + | access-denied | 403 | + | lock-denied | 409 | + | resource-denied | 409 | + | rollback-failed | 500 | + | data-exists | 409 | + | data-missing | 409 | + | operation-not-supported | 501 | + | operation-failed | 500 | + | partial-operation | 500 | + | malformed-message | 400 | + +-------------------------+-------------+ + + * "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3 + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml-h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_methods_patch.h" + + +/*! Generic REST PATCH method + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * Netconf: (nc:operation="merge") + * See RFC8040 Sec 4.6 + */ +int +api_data_patch(clicon_handle h, + FCGX_Request *r, + char *api_path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data) +{ + notimplemented(r); + return 0; +} + diff --git a/apps/restconf/restconf_methods_patch.h b/apps/restconf/restconf_methods_patch.h new file mode 100644 index 00000000..7149b07e --- /dev/null +++ b/apps/restconf/restconf_methods_patch.h @@ -0,0 +1,48 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation + */ + + +#ifndef _RESTCONF_METHODS_PATCH_H_ +#define _RESTCONF_METHODS_PATCH_H_ + +/* + * Prototypes + */ +int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path, + cvec *pcvec, int pi, + cvec *qvec, char *data); + +#endif /* _RESTCONF_METHODS_PATCH_H_ */ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index 104b37ce..d88bbc75 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -62,7 +62,6 @@ #include /* Need to be after clixon_xml.h due to attribute format */ #include "restconf_lib.h" -#include "restconf_methods.h" #include "restconf_methods_post.h" /*! Generic REST POST method @@ -129,9 +128,10 @@ api_data_post(clicon_handle h, cxobj *xerr = NULL; /* malloced must be freed */ cxobj *xe; /* dont free */ char *username; + int nullspec = 0; int ret; - clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", + clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"", __FUNCTION__, api_path, data); if ((yspec = clicon_dbspec_yang(h)) == NULL){ @@ -178,6 +178,15 @@ api_data_post(clicon_handle h, } } else { + /* Data here cannot cannot (always) be Yang populated since it is + * loosely hanging without top symbols. + * And if it is not yang populated, it cant be translated properly + * from JSON to XML. + * Therefore, yang population is done later after addsub below + * Further complication is that if data is root resource, then it will + * work, so I need to check below that it didnt. + * THIS could be simplified. + */ if ((ret = json_parse_str(data, yspec, &xdata0, &xerr)) < 0){ if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) goto done; @@ -245,9 +254,24 @@ api_data_post(clicon_handle h, /* Replace xbot with x, ie bottom of api-path with data */ if (xml_addsub(xbot, xdata) < 0) goto done; - /* xbot is already populated, resolve yang for added xdata too */ - if (xml_spec_populate(xdata, yspec) < 0) + /* xbot is already populated, resolve yang for added xdata too + */ + nullspec = (xml_spec(xdata) == NULL); + if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0) goto done; + if (!parse_xml && nullspec){ + /* json2xml decode may not have been done above in json_parse, + need to be done here instead + UNLESS it is a root resource, then json-parse does right + */ + if ((ret = json2xml_decode(xdata, &xerr)) < 0) + goto done; + if (ret == 0){ + if (api_return_err(h, r, xerr, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } /* Create text buffer for transfer to backend */ if ((cbx = cbuf_new()) == NULL){ diff --git a/include/clixon_custom.h b/include/clixon_custom.h index 424790fb..c60b8101 100644 --- a/include/clixon_custom.h +++ b/include/clixon_custom.h @@ -57,4 +57,4 @@ * The easy way to do this is to always generate all prefix/namespace bindings * on the top-level for the modules involved in the netconf operation. */ -#undef IDENTITYREF_KLUDGE +#define IDENTITYREF_KLUDGE diff --git a/lib/clixon/clixon_json.h b/lib/clixon/clixon_json.h index 30488d0b..f218e6b2 100644 --- a/lib/clixon/clixon_json.h +++ b/lib/clixon/clixon_json.h @@ -41,6 +41,7 @@ /* * Prototypes */ +int json2xml_decode(cxobj *x, cxobj **xerr); int xml2json_cbuf(cbuf *cb, cxobj *x, int pretty); int xml2json_cbuf_vec(cbuf *cb, cxobj **vec, size_t veclen, int pretty); int xml2json(FILE *f, cxobj *x, int pretty); diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index 4070eac2..a0bd8498 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -170,6 +170,7 @@ yang_stmt *ys_module(yang_stmt *ys); yang_stmt *ys_real_module(yang_stmt *ys); yang_stmt *ys_spec(yang_stmt *ys); yang_stmt *yang_find_module_by_prefix(yang_stmt *ys, char *prefix); +yang_stmt *yang_find_module_by_prefix_yspec(yang_stmt *yspec, char *prefix); yang_stmt *yang_find_module_by_namespace(yang_stmt *yspec, char *namespace); yang_stmt *yang_find_module_by_name(yang_stmt *yspec, char *name); yang_stmt *yang_find(yang_stmt *yn, int keyword, const char *argument); @@ -178,6 +179,7 @@ yang_stmt *yang_find_datanode(yang_stmt *yn, char *argument); yang_stmt *yang_find_schemanode(yang_stmt *yn, char *argument); char *yang_find_myprefix(yang_stmt *ys); char *yang_find_mynamespace(yang_stmt *ys); +int yang_find_prefix_by_namespace(yang_stmt *ys, char *namespace, char **prefix); yang_stmt *yang_choice(yang_stmt *y); int yang_order(yang_stmt *y); int yang_print(FILE *f, yang_stmt *yn); diff --git a/lib/src/clixon_json.c b/lib/src/clixon_json.c index 609e7e07..23c9389b 100644 --- a/lib/src/clixon_json.c +++ b/lib/src/clixon_json.c @@ -36,6 +36,10 @@ * http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf */ +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif + #include #include #include @@ -64,6 +68,7 @@ #include "clixon_xml.h" #include "clixon_xml_sort.h" #include "clixon_xml_map.h" +#include "clixon_xml_nsctx.h" /* namespace context */ #include "clixon_netconf_lib.h" #include "clixon_json.h" #include "clixon_json_parse.h" @@ -224,7 +229,8 @@ array_eval(cxobj *xprev, } /*! Escape a json string as well as decode xml cdata - * And a + * @param[out] cb cbuf (encoded) + * @param[in] str string (unencoded) */ static int json_str_escape_cdata(cbuf *cb, @@ -272,25 +278,287 @@ json_str_escape_cdata(cbuf *cb, return retval; } -/*! If set, quoute the json value with double quotes - * @þaram[in] xb XML body object - @ @retval 0 Value should not be quouted, XML value is int, boolean,.. - @ @retval 1 Value should be quouted, XML value is string,.. +/*! Decode types from JSON to XML identityrefs + * Assume an xml tree where prefix:name have been split into "module":"name" + * In other words, from JSON RFC7951 to XML namespace trees + * @param[in] x XML tree. Must be yang populated. + * @param[in] yspec Yang spec + * @param[out] xerr Reason for invalid tree returned as netconf err msg or NULL + * @retval 1 OK + * @retval 0 Invalid, wrt namespace. xerr set + * @retval -1 Error + * @see RFC7951 Sec 4 and 6.8 */ static int -jsonvaluestr(cxobj *xb) +json2xml_decode_identityref(cxobj *x, + yang_stmt *y, + cxobj **xerr) { - int retval = 1; - cxobj *xp; - yang_stmt *yp; - enum rfc_6020 keyword; + int retval = -1; + char *namespace; + char *body; + cxobj *xb; + cxobj *xa; + char *prefix = NULL; + char *id = NULL; + yang_stmt *ymod; + yang_stmt *yspec; + cvec *nsc = NULL; + char *prefix2 = NULL; + cbuf *cbv = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + yspec = ys_spec(y); + if ((xb = xml_body_get(x)) == NULL) + goto ok; + body = xml_value(xb); + if (nodeid_split(body, &prefix, &id) < 0) + goto done; + /* prefix is a module name -> find module */ + if (prefix){ + if ((ymod = yang_find_module_by_name(yspec, prefix)) != NULL){ + namespace = yang_find_mynamespace(ymod); + /* Is this namespace in the xml context? + * (yes) use its prefix (unless it is NULL) + * (no) insert a xmlns: statement + * Get the whole namespace context from x + */ + if (xml_nsctx_node(x, &nsc) < 0) + goto done; + clicon_debug(1, "%s prefix:%s body:%s namespace:%s", + __FUNCTION__, prefix, body, namespace); + if (!xml_nsctx_get_prefix(nsc, namespace, &prefix2)){ + /* (no) insert a xmlns: statement + * Get yang prefix from import statement of my mod */ + if (yang_find_prefix_by_namespace(y, namespace, &prefix2) == 0){ +#ifndef IDENTITYREF_KLUDGE + /* Just get the prefix from the module's own namespace */ + if (netconf_unknown_namespace_xml(xerr, "application", + namespace, + "No local prefix corresponding to namespace") < 0) + goto done; + goto fail; +#endif + } + /* if prefix2 is NULL here, we get the canonical prefix */ + if (prefix2 == NULL) + prefix2 = yang_find_myprefix(ymod); + /* Add "xmlns:prefix2=namespace" */ + if ((xa = xml_new(prefix2, x, NULL)) == NULL) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_prefix_set(xa, "xmlns") < 0) + goto done; + if (xml_value_set(xa, namespace) < 0) + goto done; + } + /* Here prefix2 is valid and can be NULL + Change body prefix to prefix2:id */ + if ((cbv = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (prefix2) + cprintf(cbv, "%s:%s", prefix2, id); + else + cprintf(cbv, "%s", id); + if (xml_value_set(xb, cbuf_get(cbv)) < 0) + goto done; + } + else{ + if (netconf_unknown_namespace_xml(xerr, "application", + prefix, + "No module corresponding to prefix") < 0) + goto done; + goto fail; + } + } /* prefix */ + ok: + retval = 1; + done: + if (prefix) + free(prefix); + if (id) + free(id); + if (nsc) + xml_nsctx_free(nsc); + if (cbv) + cbuf_free(cbv); + return retval; + fail: + retval = 0; + goto done; +} + +/*! Decode leaf/leaf_list types from JSON to XML after parsing and yang + * + * Assume an xml tree where prefix:name have been split into "module":"name" + * In other words, from JSON RFC7951 to XML namespace trees + * + * @param[in] x XML tree. Must be yang populated. After json parsing + * @param[in] yspec Yang spec + * @param[out] xerr Reason for invalid tree returned as netconf err msg or NULL + * @retval 1 OK + * @retval 0 Invalid, wrt namespace. xerr set + * @retval -1 Error + * @see RFC7951 Sec 4 and 6.8 + */ +int +json2xml_decode(cxobj *x, + cxobj **xerr) +{ + int retval = -1; + yang_stmt *y; + enum rfc_6020 keyword; + cxobj *xc; + int ret; + yang_stmt *ytype; + + if ((y = xml_spec(x)) != NULL){ + keyword = yang_keyword_get(y); + if (keyword == Y_LEAF || keyword == Y_LEAF_LIST){ + if (yang_type_get(y, NULL, &ytype, NULL, NULL, NULL, NULL, NULL) < 0) + goto done; + if (ytype) + if (strcmp(yang_argument_get(ytype),"identityref")==0){ + if ((ret = json2xml_decode_identityref(x, y, xerr)) < 0) + goto done; + if (ret == 0) + goto fail; + } + } + } + xc = NULL; + while ((xc = xml_child_each(x, xc, CX_ELMNT)) != NULL){ + if ((ret = json2xml_decode(xc, xerr)) < 0) + goto done; + if (ret == 0) + goto fail; + } + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + +/*! Encode leaf/leaf_list identityref type from XML to JSON + * @param[in] x XML body node + * @param[in] body body string + * @param[in] ys Yang spec of parent + * @param[out] cb Encoded string + */ +static int +xml2json_encode_identityref(cxobj *xb, + char *body, + yang_stmt *yp, + cbuf *cb) +{ + int retval = -1; + char *prefix = NULL; + char *id = NULL; + char *namespace = NULL; + yang_stmt *ymod; + yang_stmt *yspec; + yang_stmt *my_ymod; + + my_ymod = ys_module(yp); + yspec = ys_spec(yp); + if (nodeid_split(body, &prefix, &id) < 0) + goto done; + /* prefix is xml local -> get namespace */ + if (xml2ns(xb, prefix, &namespace) < 0) + goto done; + /* We got the namespace, now get the module */ + // clicon_debug(1, "%s body:%s prefix:%s namespace:%s", __FUNCTION__, body, prefix, namespace); +#ifdef IDENTITYREF_KLUDGE + if (namespace == NULL){ + /* If we dont find namespace here, we assume it is because of a missing + * xmlns that should be there, as a kludge we search for its (own) + * prefix in mymodule. + */ + if ((ymod = yang_find_module_by_prefix_yspec(yspec, prefix)) != NULL) + cprintf(cb, "%s:%s", yang_argument_get(ymod), id); + else + cprintf(cb, "%s", id); + } + else +#endif + { + if ((ymod = yang_find_module_by_namespace(yspec, namespace)) != NULL){ + + if (ymod == my_ymod) + cprintf(cb, "%s", id); + else{ + cprintf(cb, "%s:%s", yang_argument_get(ymod), id); + } + } + else + cprintf(cb, "%s", id); + } + retval = 0; + done: + if (prefix) + free(prefix); + if (id) + free(id); + return retval; +} + +/*! Encode leaf/leaf_list types from XML to JSON + * @param[in] x XML body + * @param[in] ys Yang spec of parent + * @param[out] cb0 Encoded string + */ +static int +xml2json_encode(cxobj *xb, + cbuf *cb0) +{ + int retval = -1; + cxobj *xp; + yang_stmt *yp; + enum rfc_6020 keyword; + yang_stmt *ytype; + char *restype; /* resolved type */ + char *origtype=NULL; /* original type */ + char *body; + enum cv_type cvtype; + int quote = 1; /* Quote value w string: "val" */ + cbuf *cb = NULL; /* the variable itself */ + + if ((cb = cbuf_new()) ==NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + body = xml_value(xb); if ((xp = xml_parent(xb)) == NULL || - (yp = xml_spec(xp)) == NULL) - goto done; /* unknown */ + (yp = xml_spec(xp)) == NULL){ + cprintf(cb, "%s", body); + goto ok; /* unknown */ + } keyword = yang_keyword_get(yp); - if ((keyword == Y_LEAF || keyword == Y_LEAF_LIST)) - switch (yang_type2cv(yp)){ + switch (keyword){ + case Y_LEAF: + case Y_LEAF_LIST: + if (yang_type_get(yp, &origtype, &ytype, NULL, NULL, NULL, NULL, NULL) < 0) + goto done; + restype = ytype?yang_argument_get(ytype):NULL; + cvtype = yang_type2cv(yp); + switch (cvtype){ + case CGV_STRING: + if (ytype){ + if (strcmp(restype, "identityref")==0){ + if (xml2json_encode_identityref(xb, body, yp, cb) < 0) + goto done; + } + else + cprintf(cb, "%s", body); + } + else + cprintf(cb, "%s", body); + break; case CGV_INT8: case CGV_INT16: case CGV_INT32: @@ -301,18 +569,42 @@ jsonvaluestr(cxobj *xb) case CGV_UINT64: case CGV_DEC64: case CGV_BOOL: - retval = 0; + cprintf(cb, "%s", body); + quote = 0; break; default: - break; + cprintf(cb, "%s", body); } + break; + default: + cprintf(cb, "%s", body); + break; + } + ok: + /* write into original cb0 + * includign quoting and encoding + */ + if (quote){ + cprintf(cb0, "\""); + json_str_escape_cdata(cb0, cbuf_get(cb)); + } + else + cprintf(cb0, "%s", cbuf_get(cb)); + if (quote) + cprintf(cb0, "\""); + retval = 0; done: + if (cb) + cbuf_free(cb); + if (origtype) + free(origtype); return retval; } /*! Do the actual work of translating XML to JSON * @param[out] cb Cligen text buffer containing json on exit * @param[in] x XML tree structure containing XML to translate + * @param[in] yp Parent yang spec needed for body * @param[in] arraytype Does x occur in a array (of its parent) and how? * @param[in] level Indentation level * @param[in] pretty Pretty-print output (2 means debug) @@ -381,17 +673,10 @@ xml2json1_cbuf(cbuf *cb, arraytype2str(arraytype), childtype2str(childt)); switch(arraytype){ - case BODY_ARRAY:{ - if (jsonvaluestr(x)) { /* Only print quotation if string-type */ - cprintf(cb, "\""); - if (json_str_escape_cdata(cb, xml_value(x)) < 0) - goto done; - cprintf(cb, "\""); - } - else /* No quotation marks */ - cprintf(cb, "%s", xml_value(x)); + case BODY_ARRAY: /* Only place in fn where body is printed */ + if (xml2json_encode(x, cb) < 0) + goto done; break; - } case NO_ARRAY: if (!flat){ cprintf(cb, "%*s\"", pretty?(level*JSON_INDENT):0, ""); @@ -739,7 +1024,7 @@ xml2json_vec(FILE *f, /*! Translate from JSON module:name to XML default ns: xmlns="uri" recursively * Assume an xml tree where prefix:name have been split into "module":"name" - * In other words, from JSON RFC7951 to XML namespace trees + * In other words, from JSON to XML namespace trees * * @param[in] yspec Yang spec * @param[in,out] x XML tree. Translate it in-line @@ -749,8 +1034,9 @@ xml2json_vec(FILE *f, * @retval -1 Error * @note the opposite - xml2ns is made inline in xml2json1_cbuf * Example: --> + * @see RFC7951 Sec 4 */ -int +static int json_xmlns_translate(yang_stmt *yspec, cxobj *x, cxobj **xerr) @@ -799,7 +1085,7 @@ json_xmlns_translate(yang_stmt *yspec, retval = 0; goto done; } - + /*! Parse a string containing JSON and return an XML tree * * Parsing using yacc according to JSON syntax. Names with : @@ -809,17 +1095,17 @@ json_xmlns_translate(yang_stmt *yspec, * @param[in] yspec If set, also do yang validation * @param[in] name Log string, typically filename * @param[out] xt XML top of tree typically w/o children on entry (but created) - * @param[out] xerr Reason for invalid returned as netconf err msg + * @param[out] xerr Reason for invalid returned as netconf err msg * * @see _xml_parse for XML variant - * @retval 1 OK and valid - * @retval 0 Invalid (only if yang spec) - * @retval -1 Error with clicon_err called + * @retval 1 OK and valid + * @retval 0 Invalid (only if yang spec) + * @retval -1 Error with clicon_err called * @see http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf * @see RFC 7951 */ static int -json_parse(char *str, +json_parse(char *str, yang_stmt *yspec, const char *name, cxobj *xt, @@ -855,6 +1141,12 @@ json_parse(char *str, goto done; if (xml_apply0(xt, CX_ELMNT, xml_sort, NULL) < 0) goto done; + /* Now find leafs with identityrefs (+transitive) and translate + * prefixes in values to XML namespaces */ + if ((ret = json2xml_decode(xt, xerr)) < 0) + goto done; + if (ret == 0) /* XXX necessary? */ + goto fail; } retval = 1; done: @@ -871,7 +1163,7 @@ json_parse(char *str, * * @param[in] str String containing JSON * @param[in] yspec Yang specification, or NULL - * @param[in,out] xt On success a top of XML parse tree is created with name 'top' + * @param[in,out] xt Top object, if not exists, on success it is created with name 'top' * @param[out] xerr Reason for invalid returned as netconf err msg * * @code diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index 992a960e..e7d7e371 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -279,7 +279,7 @@ nscache_set(cxobj *x, * @param[out] namespace URI namespace (or NULL). Note pointer into xml tree * @retval 0 OK * @retval -1 Error - * @see xmlns_check XXX can these be merged? + * @see xmlns_check * @see xmlns_set cache is set * @note, this function uses a cache. */ @@ -307,8 +307,12 @@ xml2ns(cxobj *x, } /* If no parent, return default namespace if defined */ #ifdef USE_NETCONF_NS_AS_DEFAULT - else - ns = NETCONF_BASE_NAMESPACE; + else{ + if (prefix == NULL) + ns = NETCONF_BASE_NAMESPACE; + else + ns = NULL; + } #endif } /* Set default namespace cache (since code is at this point, diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index df56244f..565f1943 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -332,24 +332,6 @@ validate_leafref(cxobj *xt, goto done; } -/* Get module from its own prefix - * This is really not a valid usecase, a kludge for the identityref derived - * list workaround (IDENTITYREF_KLUDGE) - */ -static yang_stmt * -yang_find_module_by_prefix_yspec(yang_stmt *yspec, - char *prefix) -{ - yang_stmt *ymod = NULL; - yang_stmt *yprefix; - - while ((ymod = yn_each(yspec, ymod)) != NULL) - if (ymod->ys_keyword == Y_MODULE && - (yprefix = yang_find(ymod, Y_PREFIX, NULL)) != NULL && - strcmp(yang_argument_get(yprefix), prefix) == 0) - return ymod; - return NULL; -} /*! Validate xml node of type identityref, ensure value is a defined identity * Check if a given node has value derived from base identity. This is @@ -2340,6 +2322,7 @@ xml_spec_populate_rpc(clicon_handle h, * xml_apply(xc, CX_ELMNT, xml_spec_populate, yspec) * @endcode */ +#undef DEBUG int xml_spec_populate(cxobj *x, void *arg) @@ -2355,17 +2338,36 @@ xml_spec_populate(cxobj *x, yspec = (yang_stmt*)arg; xp = xml_parent(x); name = xml_name(x); +#ifdef DEBUG + clicon_debug(1, "%s name:%s", __FUNCTION__, name); +#endif if (xp && (yparent = xml_spec(xp)) != NULL) y = yang_find_datanode(yparent, name); else if (yspec){ if (ys_module_by_xml(yspec, x, &ymod) < 0) goto done; /* ymod is "real" module, name may belong to included submodule */ - if (ymod != NULL) + if (ymod != NULL){ +#ifdef DEBUG + clicon_debug(1, "%s %s mod:%s", __FUNCTION__, name, yang_argument_get(ymod)); +#endif y = yang_find_schemanode(ymod, name); + } +#ifdef DEBUG + else + clicon_debug(1, "%s %s mod:NULL", __FUNCTION__, name); +#endif } - if (y) + if (y) { +#ifdef DEBUG + clicon_debug(1, "%s y:%s", __FUNCTION__, yang_argument_get(y)); +#endif xml_spec_set(x, y); + } +#ifdef DEBUG + else + clicon_debug(1, "%s y:NULL", __FUNCTION__); +#endif retval = 0; done: return retval; diff --git a/lib/src/clixon_yang.c b/lib/src/clixon_yang.c index 7496f465..30c11d57 100644 --- a/lib/src/clixon_yang.c +++ b/lib/src/clixon_yang.c @@ -795,6 +795,64 @@ yang_find_mynamespace(yang_stmt *ys) return namespace; } +/*! Given a yang statement and namespace, find local prefix valid in module + * This is useful if you want to make a "reverse" lookup, you know the + * (global) namespace of a module, but you do not know the local prefix + * used to access it in XML. + * @param[in] ys Yang statement in module tree (or module itself) + * @param[in] namespace Namspace URI as char* pointer into yang tree + * @param[out] prefix Local prefix to access module with (direct pointer) + * @retval 0 not found + * @retval -1 found + * @code + * @note prefix NULL is not returned, if own module, then return its prefix + * char *pfx = yang_find_prefix_by_namespace(ys, "urn:example:clixon", &prefix); + * @endcode + */ +int +yang_find_prefix_by_namespace(yang_stmt *ys, + char *namespace, + char **prefix) +{ + int retval = 0; /* not found */ + yang_stmt *my_ymod; /* My module */ + char *myns; /* My ns */ + yang_stmt *yspec; + yang_stmt *ymod; + char *modname = NULL; + yang_stmt *yimport; + yang_stmt *yprefix; + + clicon_debug(1, "%s", __FUNCTION__); + /* First check if namespace is my own module */ + myns = yang_find_mynamespace(ys); + if (strcmp(myns, namespace) == 0){ + *prefix = yang_find_myprefix(ys); /* or NULL? */ + goto found; + } + /* Next, find namespaces in imported modules */ + yspec = ys_spec(ys); + if ((ymod = yang_find_module_by_namespace(yspec, namespace)) == NULL) + goto notfound; + modname = yang_argument_get(ymod); + my_ymod = ys_module(ys); + /* Loop through import statements to find a match with ymod */ + yimport = NULL; + while ((yimport = yn_each(my_ymod, yimport)) != NULL) { + if (yang_keyword_get(yimport) == Y_IMPORT && + strcmp(modname, yang_argument_get(yimport)) == 0){ /* match */ + yprefix = yang_find(yimport, Y_PREFIX, NULL); + *prefix = yang_argument_get(yprefix); + goto found; + } + } + notfound: + return retval; + found: + assert(*prefix); + return 1; +} + /*! If a given yang stmt has a choice/case as parent, return the choice statement */ yang_stmt * @@ -985,16 +1043,8 @@ ys_module_by_xml(yang_stmt *ysp, if (ymodp) *ymodp = NULL; prefix = xml_prefix(xt); - if (prefix){ - /* Get namespace for prefix */ - if (xml2ns(xt, prefix, &namespace) < 0) - goto done; - } - else{ - /* Get default namespace */ - if (xml2ns(xt, NULL, &namespace) < 0) - goto done; - } + if (xml2ns(xt, prefix, &namespace) < 0) /* prefix may be NULL */ + goto done; /* No namespace found, give up */ if (namespace == NULL) goto ok; @@ -1134,8 +1184,7 @@ yang_find_module_by_prefix(yang_stmt *ys, } yimport = NULL; while ((yimport = yn_each(my_ymod, yimport)) != NULL) { - if (yang_keyword_get(yimport) != Y_IMPORT && - yang_keyword_get(yimport) != Y_INCLUDE) + if (yang_keyword_get(yimport) != Y_IMPORT) continue; if ((yprefix = yang_find(yimport, Y_PREFIX, NULL)) != NULL && strcmp(yang_argument_get(yprefix), prefix) == 0){ @@ -1143,8 +1192,7 @@ yang_find_module_by_prefix(yang_stmt *ys, } } if (yimport){ - if ((ymod = yang_find(yspec, Y_MODULE, yang_argument_get(yimport))) == NULL && - (ymod = yang_find(yspec, Y_SUBMODULE, yang_argument_get(yimport))) == NULL){ + if ((ymod = yang_find(yspec, Y_MODULE, yang_argument_get(yimport))) == NULL){ clicon_err(OE_YANG, 0, "No module or sub-module found with prefix %s", prefix); yimport = NULL; @@ -1155,6 +1203,25 @@ yang_find_module_by_prefix(yang_stmt *ys, return ymod; } +/* Get module from its own prefix + * This is really not a valid usecase, a kludge for the identityref derived + * list workaround (IDENTITYREF_KLUDGE) + */ +yang_stmt * +yang_find_module_by_prefix_yspec(yang_stmt *yspec, + char *prefix) +{ + yang_stmt *ymod = NULL; + yang_stmt *yprefix; + + while ((ymod = yn_each(yspec, ymod)) != NULL) + if (ymod->ys_keyword == Y_MODULE && + (yprefix = yang_find(ymod, Y_PREFIX, NULL)) != NULL && + strcmp(yang_argument_get(yprefix), prefix) == 0) + return ymod; + return NULL; +} + /*! Given a yang spec and a namespace, return yang module * * @param[in] yspec A yang specification diff --git a/test/test_json.sh b/test/test_json.sh index 9c974f67..e3994483 100755 --- a/test/test_json.sh +++ b/test/test_json.sh @@ -8,12 +8,26 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi : ${clixon_util_json:=clixon_util_json} +: ${clixon_util_xml:=clixon_util_xml} fyang=$dir/json.yang cat < $fyang module json{ prefix ex; namespace "urn:example:clixon"; + identity genre { + description + "From RFC8040 jukebox example. + Identity prefixes are translated from module-name to xml prefix"; + } + identity blues { + base genre; + } + typedef gtype{ + type identityref{ + base genre; + } + } leaf a{ type int32; } @@ -25,6 +39,14 @@ module json{ type string; } } + leaf g1 { + description "direct type"; + type identityref { base genre; } + } + leaf g2 { + description "indirect type"; + type gtype; + } } EOF @@ -68,18 +90,42 @@ expecteofeq "$clixon_util_json -jpy $fyang" 0 "$JSON" "$JSONP" JSON='{"json:a":-23}' new "json leaf back to json" -expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" +expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" "$JSON" JSON='{"json:c":{"a":937}}' new "json parse container back to json" -expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" +expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" "$JSON" -# This should work +# identities translation json -> xml is tricky wrt prefixes, json uses module +# name, xml uses xml namespace prefixes (or default) +JSON='{"json:g1":"json:blues"}' + +new "json identity to xml" +expecteofx "$clixon_util_json -y $fyang" 0 "$JSON" 'blues' + +new "json identity back to json" +expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" '{"json:g1":"blues"}' + +new "xml identity with explicit ns to json" +expecteofx "$clixon_util_xml -ovjy $fyang" 0 'ex:blues' '{"json:g1":"blues"}' + +# Same with indirect type +JSON='{"json:g2":"json:blues"}' + +new "json indirect identity to xml" +expecteofx "$clixon_util_json -y $fyang" 0 "$JSON" 'blues' + +new "json indirect identity back to json" +expecteofx "$clixon_util_json -jy $fyang" 0 "$JSON" '{"json:g2":"blues"}' + +new "xml indirect identity with explicit ns to json" +expecteofx "$clixon_util_xml -ojvy $fyang" 0 'ex:blues' '{"json:g2":"blues"}' + +# XXX CDATA translation, should work bit does not if false; then JSON='{"json:c": {"s": " x & x < y ]]>"}}' new "json parse cdata xml" expecteofx "$clixon_util_json -j -y $fyang" 0 "$JSON" "$JSON" fi - rm -rf $dir diff --git a/test/test_perf_state.sh b/test/test_perf_state.sh index 17d3ef1b..a79bfffc 100755 --- a/test/test_perf_state.sh +++ b/test/test_perf_state.sh @@ -98,8 +98,8 @@ new "netconf get $perfreq single reqs" done | $clixon_netconf -qf $cfg > /dev/null; } 2>&1 | awk '/real/ {print $2}' # RESTCONF get -new "restconf get test single req XXX" -expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface":[{"name":"e1","type":"ex:eth","enabled":true,"oper-status":"up"}]} +new "restconf get test single req" +expecteq "$(curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces/interface=e1)" 0 '{"ietf-interfaces:interface":[{"name":"e1","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]} ' new "restconf get $perfreq single reqs" diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 4f0c6b36..aa579de2 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -160,16 +160,16 @@ expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" -expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created' +expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created' new "restconf Re-add subtree eth/0/0 which should give error" -expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' +expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' # XXX Cant get this to work -#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"ex:eth\",\"enabled\":true}}} http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' +#expecteq "$(curl -s -X POST -d {\"interfaces\":{\"interface\":{\"name\":\"eth/0/0\",\"type\":\"clixon-example:eth\",\"enabled\":true}}} http://localhost/restconf/data)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' new "restconf Check interfaces eth/0/0 added" -expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}}} +expectfn "curl -s -X GET http://localhost/restconf/data/ietf-interfaces:interfaces" 0 '{"ietf-interfaces:interfaces":{"interface":\[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}\]}}} ' new "restconf delete interfaces" @@ -179,15 +179,12 @@ new "restconf Check empty config" expectfn "curl -sG http://localhost/restconf/data/clixon-example:state" 0 "$state " -# XXX: gives -# new "restconf Add interfaces subtree eth/0/0 using POST" -expectfn 'curl -s -X POST -d {"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}} http://localhost/restconf/data/ietf-interfaces:interfaces' 0 "" -# XXX cant get this to work -#expecteq "$(curl -s -X POST -d '{"interface":{"name":"eth/0/0","type\":"ex:eth","enabled":true}}' http://localhost/restconf/data/interfaces)" 0 "" +expectpart "$(curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}')" 0 "" +#expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces -d {"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' 0 "" new "restconf Check eth/0/0 added config" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf Check eth/0/0 added state" @@ -195,7 +192,7 @@ expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state)" 0 ' ' new "restconf Re-post eth/0/0 which should generate error" -expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' +expecteq "$(curl -s -X POST -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}} ' new "Add leaf description using POST" expecteq "$(curl -s -X POST -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" @@ -204,7 +201,7 @@ new "Add nothing using POST" expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":" on line 1: syntax error at or before:' new "restconf Check description added" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"ex:eth","enabled":true,"oper-status":"up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf delete eth/0/0" @@ -217,10 +214,10 @@ new "restconf Re-Delete eth/0/0 using none should generate error" expecteq "$(curl -s -X DELETE http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-missing","error-severity":"error","error-message":"Data does not exist; cannot delete resource"}}} ' new "restconf Add subtree eth/0/0 using PUT" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" +expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" new "restconf get subtree" -expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"ex:eth","enabled":true,"oper-status":"up"}]}} +expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} ' new "restconf rpc using POST json" @@ -262,10 +259,10 @@ if [ -z "$match" ]; then fi new "restconf Add subtree without key (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}} ' +expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"malformed key, expected '"'"'=restval'"'"'"}}} ' new "restconf Add subtree with too many keys (expected error)" -expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}} ' +expecteq "$(curl -s -X PUT -d '{"ietf-interfaces:interface":{"name":"eth/0/0","type":"clixon-example:eth","enabled":true}}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=a,b)" 0 '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"List key interface length mismatch"}}} ' new "Kill restconf daemon" stop_restconf diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 18703884..2429b6e3 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -337,13 +337,12 @@ expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' ht new "4.5. PUT replace content" # XXX should be: jbox:alternative --> example-jukebox:alternative -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"jbox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" new "4.5. PUT replace content (xml encoding)" expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" -new "4.5. PUT create new" -# XXX should be: jbox:alternative --> example-jukebox:alternative +new "4.5. PUT create new identity" expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" diff --git a/test/test_xml.sh b/test/test_xml.sh index c3943ea5..8b51ccb1 100755 --- a/test/test_xml.sh +++ b/test/test_xml.sh @@ -8,6 +8,8 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi : ${clixon_util_xml:=clixon_util_xml -o} # -o is output +sleep 1 # mysterious fail, maybe this helps? + new "xml parse" expecteof "$clixon_util_xml" 0 "" "^$" diff --git a/util/clixon_util_json.c b/util/clixon_util_json.c index 9b8e338b..5ad62f2a 100644 --- a/util/clixon_util_json.c +++ b/util/clixon_util_json.c @@ -74,7 +74,7 @@ usage(char *argv0) "where options are\n" "\t-h \t\tHelp\n" "\t-D \tDebug\n" - "\t-j \t\tOutput as JSON\n" + "\t-j \t\tOutput as JSON (default is as XML)\n" "\t-l \tLog on (s)yslog, std(e)rr, std(o)ut (stderr is default)\n" "\t-p \t\tPretty-print output\n" "\t-y \tyang filename to parse (must be stand-alone)\n" , From dbfd92846c795e3cf0b108eb7d411d16c13e698d Mon Sep 17 00:00:00 2001 From: Olof Hagsand Date: Sun, 28 Jul 2019 19:00:12 +0000 Subject: [PATCH 06/10] test script parameter interfence --- test/test_stream.sh | 16 +++++----- test/test_xml.sh | 74 ++++++++++++++++++++++----------------------- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/test/test_stream.sh b/test/test_stream.sh index 70713344..30a6418e 100755 --- a/test/test_stream.sh +++ b/test/test_stream.sh @@ -158,7 +158,7 @@ expectwait "$clixon_netconf -qf $cfg" 'EXAMPLE$NOW]]>]]>" '^]]>]]>20' 10 -sleep 2 +sleep 1 # # 2. Restconf RFC8040 stream testing @@ -167,11 +167,11 @@ new "2. Restconf RFC8040 stream testing" new "restconf event stream discovery RFC8040 Sec 6.2" expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams" 0 '{"ietf-restconf-monitoring:streams":{"stream":\[{"name":"EXAMPLE","description":"Example event stream","replay-support":true,"access":\[{"encoding":"xml","location":"https://localhost/streams/EXAMPLE"}\]}\]}' -sleep 2 +sleep 1 new "restconf subscribe RFC8040 Sec 6.3, get location" expectfn "curl -s -X GET http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/streams/stream=EXAMPLE/access=xml/location" 0 '{"ietf-restconf-monitoring:location":"https://localhost/streams/EXAMPLE"}' -sleep 2 +sleep 1 # Restconf stream subscription RFC8040 Sec 6.3 # Start Subscription w error new "restconf monitor event nonexist stream" @@ -191,7 +191,7 @@ if [ $nr -lt 1 -o $nr -gt 2 ]; then err 2 "$nr" fi -sleep 2 +sleep 1 # 2b) start subscription 8s - stoptime after 5s - expect 1-2 notifications new "2b) start subscriptions 8s - stoptime after 5s - expect 1-2 notifications" @@ -206,7 +206,7 @@ if [ $nr -lt 1 -o $nr -gt 2 ]; then err 1 "$nr" fi -sleep 2 +sleep 1 # 2c new "2c) start sub 8s - replay from start -8s - expect 3-4 notifications" @@ -221,7 +221,7 @@ if [ $nr -lt 3 -o $nr -gt 4 ]; then err 4 "$nr" fi -sleep 2 +sleep 1 # 2d) start sub 8s - replay from start -8s to stop +4s - expect 3 notifications new "2d) start sub 8s - replay from start -8s to stop +4s - expect 3 notifications" @@ -236,7 +236,7 @@ if [ $nr -lt 4 -o $nr -gt 10 ]; then err 6 "$nr" fi -sleep 2 +sleep 1 # 2e) start sub 8s - replay from -90s w retention 60s - expect 10 notifications new "2e) start sub 8s - replay from -90s w retention 60s - expect 10 notifications" @@ -252,7 +252,7 @@ if [ $nr -lt 9 -o $nr -gt 14 ]; then err 10 "$nr" fi -sleep 2 +sleep 1 # Try parallell # start background job diff --git a/test/test_xml.sh b/test/test_xml.sh index 8b51ccb1..fa1b2939 100755 --- a/test/test_xml.sh +++ b/test/test_xml.sh @@ -6,41 +6,39 @@ # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi -: ${clixon_util_xml:=clixon_util_xml -o} # -o is output - -sleep 1 # mysterious fail, maybe this helps? +: ${clixon_util_xml:="clixon_util_xml"} new "xml parse" -expecteof "$clixon_util_xml" 0 "" "^$" +expecteof "$clixon_util_xml -o" 0 "" "^$" new "xml parse to json" -expecteof "$clixon_util_xml -j" 0 "" '{"a":{"b":null}}' +expecteof "$clixon_util_xml -oj" 0 "" '{"a":{"b":null}}' new "xml parse strange names" -expecteof "$clixon_util_xml" 0 "<_->" "<_->" +expecteof "$clixon_util_xml -o" 0 "<_->" "<_->" new "xml parse name errors" -expecteof "$clixon_util_xml" 255 "<-a/>" "" +expecteof "$clixon_util_xml -o" 255 "<-a/>" "" new "xml parse name errors" -expecteof "$clixon_util_xml" 255 "<9/>" "" +expecteof "$clixon_util_xml -o" 255 "<9/>" "" new "xml parse name errors" -expecteof "$clixon_util_xml" 255 "" "" +expecteof "$clixon_util_xml -o" 255 "" "" LF=' ' new "xml parse content with CR LF -> LF, CR->LF (see https://www.w3.org/TR/REC-xml/#sec-line-ends)" -ret=$(echo "a b${LF}c ${LF}d" | $clixon_util_xml) +ret=$(echo "a b${LF}c ${LF}d" | $clixon_util_xml -o) if [ "$ret" != "a${LF}b${LF}c${LF}d" ]; then err 'a$LFb$LFc' "$ret" fi new "xml simple CDATA" -expecteofx "$clixon_util_xml" 0 '' '' +expecteofx "$clixon_util_xml -o" 0 '' '' new "xml simple CDATA to json" -expecteofx "$clixon_util_xml -j" 0 '' '{"a":"a text"}' +expecteofx "$clixon_util_xml -o -j" 0 '' '{"a":"a text"}' new "xml complex CDATA" XML=$(cat <An example of escaped CENDs +expecteof "$clixon_util_xml -o" 0 "$XML" "^An example of escaped CENDs y\" so I guess that means that z > x ]]> ]]>]]>$" @@ -66,89 +64,89 @@ JSON=$(cat <Less than: < , greater than: > ampersand: & EOF ) new "xml encode <>&" -expecteof "$clixon_util_xml" 0 "$XML" "$XML" +expecteof "$clixon_util_xml -o" 0 "$XML" "$XML" new "xml encode <>& to json" -expecteof "$clixon_util_xml -j" 0 "$XML" '{"message":"Less than: < , greater than: > ampersand: & "}' +expecteof "$clixon_util_xml -oj" 0 "$XML" '{"message":"Less than: < , greater than: > ampersand: & "}' XML=$(cat <single-quote character ' represented as ' and double-quote character as " EOF ) new "xml single and double quote" -expecteof "$clixon_util_xml" 0 "$XML" "single-quote character ' represented as ' and double-quote character as \"" +expecteof "$clixon_util_xml -o" 0 "$XML" "single-quote character ' represented as ' and double-quote character as \"" JSON=$(cat <a\b" "a\b" +expecteofx "$clixon_util_xml -o" 0 "a\b" "a\b" new "xml backspace to json" -expecteofx "$clixon_util_xml -j" 0 "a\b" '{"a":"a\\b"}' +expecteofx "$clixon_util_xml -oj" 0 "a\b" '{"a":"a\\b"}' new "Double quotes for attributes" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "Single quotes for attributes (returns double quotes but at least parses right)" -expecteof "$clixon_util_xml" 0 "" '' +expecteof "$clixon_util_xml -o" 0 "" '' new "Mixed quotes" -expecteof "$clixon_util_xml" 0 "" '' +expecteof "$clixon_util_xml -o" 0 "" '' new "XMLdecl version" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "XMLdecl version, single quotes" -expecteof "$clixon_util_xml" 0 "" '' +expecteof "$clixon_util_xml -o" 0 "" '' new "XMLdecl version no element" -expecteof "$clixon_util_xml" 255 '' '' +expecteof "$clixon_util_xml -o" 255 '' '' new "XMLdecl no version" -expecteof "$clixon_util_xml" 255 '' '' +expecteof "$clixon_util_xml -o" 255 '' '' new "XMLdecl misspelled version" -expecteof "$clixon_util_xml -l o" 255 '' '' +expecteof "$clixon_util_xml -ol o" 255 '' '' new "XMLdecl version + encoding" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "XMLdecl version + misspelled encoding" -expecteof "$clixon_util_xml -l o" 255 '' 'syntax error: at or before: e' +expecteof "$clixon_util_xml -ol o" 255 '' 'syntax error: at or before: e' new "XMLdecl version + standalone" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "PI - Processing instruction empty" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "PI some content" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' new "prolog element misc*" -expecteof "$clixon_util_xml" 0 '' '' +expecteof "$clixon_util_xml -o" 0 '' '' # We allow it as an internal necessity for parsing of xml fragments #new "double element error" #expecteof "$clixon_util_xml" 255 '' '' new "namespace: DefaultAttName" -expecteof "$clixon_util_xml" 0 'hello' 'hello' +expecteof "$clixon_util_xml -o" 0 'hello' 'hello' new "namespace: PrefixedAttName" -expecteof "$clixon_util_xml" 0 'hello' '^hello$' +expecteof "$clixon_util_xml -o" 0 'hello' '^hello$' new "First example 6.1 from https://www.w3.org/TR/2009/REC-xml-names-20091208" XML=$(cat < EOF ) -expecteof "$clixon_util_xml" 0 "$XML" "$XML" +expecteof "$clixon_util_xml -o" 0 "$XML" "$XML" new "Second example 6.1 from https://www.w3.org/TR/2009/REC-xml-names-20091208" XML=$(cat < EOF ) -expecteof "$clixon_util_xml" 0 "$XML" "$XML" +expecteof "$clixon_util_xml -o" 0 "$XML" "$XML" rm -rf $dir From 3b93c812d435d91516cf636a7aca9afdc90988d4 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 29 Jul 2019 11:34:14 +0200 Subject: [PATCH 07/10] identity restconf mapping for augment and identity tests --- apps/restconf/restconf_methods.c | 19 +++++++- apps/restconf/restconf_methods_post.c | 3 +- lib/src/clixon_datastore_write.c | 8 ++++ lib/src/clixon_xml_map.c | 8 ++-- test/test_augment.sh | 29 +++++++++--- test/test_identity.sh | 63 +++++++++++++++++++++++++-- test/test_netconf.sh | 4 ++ test/test_restconf_jukebox.sh | 1 - 8 files changed, 118 insertions(+), 17 deletions(-) diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 6cc87ce2..ed30ddda 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -229,6 +229,7 @@ match_list_keys(yang_stmt *y, * @note restconf PUT is mapped to edit-config replace. * @see RFC8040 Sec 4.5 PUT + * @see api_data_post * @example curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 * @@ -287,6 +288,7 @@ api_data_put(clicon_handle h, int ret; char *namespace0; char *dname; + int nullspec = 0; clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"", __FUNCTION__, api_path0, data); @@ -363,6 +365,7 @@ api_data_put(clicon_handle h, goto ok; } } + /* The message-body MUST contain exactly one instance of the * expected data resource. */ @@ -378,6 +381,15 @@ api_data_put(clicon_handle h, goto ok; } xdata = xml_child_i(xdata0,0); +#if 0 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) + goto done; + clicon_debug(1, "%s DATA:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif /* If the api-path (above) defines a module, then xdata must have a prefix * and it match the module defined in api-path * This does not apply if api-path is / (no module) @@ -493,12 +505,15 @@ api_data_put(clicon_handle h, xml_purge(xbot); if (xml_addsub(xparent, xdata) < 0) goto done; + nullspec = (xml_spec(xdata) == NULL); /* xbot is already populated, resolve yang for added xdata too */ if (xml_apply0(xdata, CX_ELMNT, xml_spec_populate, yspec) < 0) goto done; - if (!parse_xml){ + if (!parse_xml && nullspec){ /* json2xml decode could not be done above in json_parse, - need to be done here instead */ + * need to be done here instead + * UNLESS it is root resource, then json-parse has already done it + */ if ((ret = json2xml_decode(xdata, &xerr)) < 0) goto done; if (ret == 0){ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index d88bbc75..e50b25ec 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -96,6 +96,7 @@ and a "409 Conflict" status-line MUST be returned. * @see RFC8040 Section 4.4 + * @see api_data_put */ int api_data_post(clicon_handle h, @@ -262,7 +263,7 @@ api_data_post(clicon_handle h, if (!parse_xml && nullspec){ /* json2xml decode may not have been done above in json_parse, need to be done here instead - UNLESS it is a root resource, then json-parse does right + UNLESS it is a root resource, then json-parse has already done it */ if ((ret = json2xml_decode(xdata, &xerr)) < 0) goto done; diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index 56fd444a..7986b50d 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -175,6 +175,14 @@ text_modify(clicon_handle h, goto done; x1name = xml_name(x1); if (yang_keyword_get(y0) == Y_LEAF_LIST || yang_keyword_get(y0) == Y_LEAF){ + /* This is a check on no further elements as a sanity check for eg + * ab + */ + if (xml_child_nr_type(x1, CX_ELMNT)){ + if (netconf_unknown_element(cbret, "application", x1name, "Leaf contains sub-element") < 0) + goto done; + goto fail; + } x1bstr = xml_body(x1); switch(op){ case OP_CREATE: diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index 565f1943..afb18f09 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -382,8 +382,11 @@ validate_identityref(cxobj *xt, } /* Get idref value. Then see if this value is derived from ytype. */ - if ((node = xml_body(xt)) == NULL) - return 0; + if ((node = xml_body(xt)) == NULL){ /* It may not be empty */ + if (netconf_bad_element_xml(xret, "application", xml_name(xt), "Identityref should not be empty") < 0) + goto done; + goto fail; + } if (nodeid_split(node, &prefix, &id) < 0) goto done; /* This is the type's base reference */ @@ -1238,7 +1241,6 @@ xml_yang_validate_list_key_only(clicon_handle h, 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 diff --git a/test/test_augment.sh b/test/test_augment.sh index a5a61153..b7a11b69 100755 --- a/test/test_augment.sh +++ b/test/test_augment.sh @@ -28,6 +28,7 @@ cat < $cfg $APPNAME /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/var/$APPNAME/$APPNAME.pidfile + false 1 /usr/local/var/$APPNAME true @@ -152,11 +153,18 @@ if [ $BE -ne 0 ]; then fi new "start backend -s init -f $cfg" start_backend -s init -f $cfg - - new "waiting" - wait_backend fi +new "kill old restconf daemon" +sudo pkill -u www-data clixon_restconf + +new "start restconf daemon" +start_restconf -f $cfg + +new "waiting" +wait_backend +wait_restconf + # mandatory-leaf See RFC7950 Sec 7.17 new "netconf set interface with augmented type and mandatory leaf" expecteof "$clixon_netconf -qf $cfg" 0 ' @@ -190,12 +198,19 @@ expecteof "$clixon_netconf -qf $cfg" 0 'mymod:you ]]>]]>' "^]]>]]>$" -new "netconf validate ok" -expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^]]>]]>$' +new "netconf commit ok" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^]]>]]>$' -new "discard" -expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" +# restconf and augment +new "restconf get augment" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK ' '{"ietf-interfaces:interfaces":{"interface":\[{"name":"e1","type":"example-augment:some-new-iftype","example-augment:mandatory-leaf":"true","example-augment:port":80,"example-augment:lport":8080},{"name":"e2","type":"fddi","example-augment:mandatory-leaf":"true","example-augment:other":"ietf-interfaces:fddi","example-augment:port":80,"example-augment:lport":8080},{"name":"e3","type":"fddi","example-augment:mandatory-leaf":"true","example-augment:me":"you","example-augment:port":80,"example-augment:lport":8080}\]}} ' + +new "restconf get augment xml" +expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 'HTTP/1.1 200 OK ' 'e1mymod:some-new-iftypetrue808080e2fdditrueif:fddi808080e3fdditruemymod:you808080' + +new "Kill restconf daemon" +stop_restconf if [ $BE -eq 0 ]; then exit # BE diff --git a/test/test_identity.sh b/test/test_identity.sh index b8005d97..489567d4 100755 --- a/test/test_identity.sh +++ b/test/test_identity.sh @@ -1,5 +1,6 @@ #!/bin/bash # Identity and identityref tests +# Example from RFC7950 Sec 7.18 and 9.10 # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -21,6 +22,7 @@ cat < $cfg example_backend.so$ /usr/local/lib/$APPNAME/netconf /usr/local/lib/$APPNAME/restconf + false /usr/local/lib/$APPNAME/cli $APPNAME /usr/local/var/$APPNAME/$APPNAME.sock @@ -148,11 +150,18 @@ if [ $BE -ne 0 ]; then fi new "start backend -s init -f $cfg" start_backend -s init -f $cfg - - new "waiting" - wait_backend fi +new "kill old restconf daemon" +sudo pkill -u www-data clixon_restconf + +new "start restconf daemon" +start_restconf -f $cfg + +new "waiting" +wait_backend +wait_restconf + new "Set crypto to aes" expecteof "$clixon_netconf -qf $cfg" 0 'aes]]>]]>' '^]]>]]>$' @@ -253,6 +262,54 @@ expectfn "$clixon_cli -1 -f $cfg -l o validate" 255 "Identityref validation fail new "netconf discard-changes" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" +# restconf and identities: +# 1. set identity in own module with restconf (PUT and POST), read it with restconf and netconf +# 2. set identity in other module with restconf , read it with restconf and netconf +# 3. set identity in other module with netconf, read it with restconf and netconf +new "restconf add own identity" +expectpart "$(curl -s -i -X PUT http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example:aes"}')" 0 'HTTP/1.1 201 Created' + +new "restconf get own identity" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example:crypto)" 0 'HTTP/1.1 200 OK' '{"example:crypto":"aes"}' + +new "netconf get own identity as set by restconf" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^aes]]>]]>$' + +new "restconf delete identity" +expectpart "$(curl -s -i -X DELETE http://localhost/restconf/data/example:crypto)" 0 "HTTP/1.1 204 No Content" + +# 2. set identity in other module with restconf , read it with restconf and netconf +new "restconf add POST instead of PUT (should fail)" +expectpart "$(curl -s -i -X POST http://localhost/restconf/data/example:crypto -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"unknown-element","error-info":{"bad-element":"crypto"},"error-severity":"error","error-message":"Leaf contains sub-element"}}}' + +new "restconf add other (des) identity using POST" +expectpart "$(curl -s -i -X POST http://localhost/restconf/data -d '{"example:crypto":"example-des:des3"}')" 0 'HTTP/1.1 201 Created' 'Location: http://localhost/restconf/data/example:crypto' + +new "restconf get other identity" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example:crypto)" 0 'HTTP/1.1 200 OK' '{"example:crypto":"example-des:des3"}' + +new "netconf get other identity" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^des:des3]]>]]>$' + +new "restconf delete identity" +expectpart "$(curl -s -i -X DELETE http://localhost/restconf/data/example:crypto)" 0 "HTTP/1.1 204 No Content" + +# 3. set identity in other module with netconf, read it with restconf and netconf +new "netconf set other identity" +expecteof "$clixon_netconf -qf $cfg" 0 'des:des3]]>]]>' "^]]>]]>$" + +new "netconf commit" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" + +new "restconf get other identity (set by netconf)" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example:crypto)" 0 'HTTP/1.1 200 OK' '{"example:crypto":"example-des:des3"}' + +new "netconf get other identity" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^des:des3]]>]]>$' + +new "Kill restconf daemon" +stop_restconf + if [ $BE -eq 0 ]; then exit # BE fi diff --git a/test/test_netconf.sh b/test/test_netconf.sh index 3ccd6c08..1ece0926 100755 --- a/test/test_netconf.sh +++ b/test/test_netconf.sh @@ -139,6 +139,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^eth2ex:ethmerge]]>]]>' "^]]>]]>$" +# Note, the type here is non-existant identityref, fails on validation new "netconf edit ampersand encoding(<&): name:'eth&' type:'t<>'" expecteof "$clixon_netconf -qf $cfg" 0 'eth&t<>]]>]]>' '^]]>]]>$' @@ -201,6 +202,9 @@ expecteof "$clixon_netconf -qf $cfg" 0 ' new "netconf client-side rpc" expecteof "$clixon_netconf -qf $cfg" 0 'val42]]>]]>' '^val42]]>]]>$' +new "netconf extra leaf in leaf should fail" +expecteof "$clixon_netconf -qf $cfg" 0 'e0e1]]>]]>' '^applicationunknown-elementnameerrorLeaf contains sub-element]]>]]>$' + if [ $BE -eq 0 ]; then exit # BE fi diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 2429b6e3..f36c96d6 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -336,7 +336,6 @@ new "B.2.1. Add Data Resources again (conflict - not in RFC)" expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 409 Conflict" new "4.5. PUT replace content" -# XXX should be: jbox:alternative --> example-jukebox:alternative expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" new "4.5. PUT replace content (xml encoding)" From d7a8cf5b0c35f2af7a61a131e735253ae8c71edc Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Mon, 29 Jul 2019 15:14:24 +0200 Subject: [PATCH 08/10] json empty type null encoded as [null] --- CHANGELOG.md | 6 ++++-- apps/restconf/restconf_methods_get.c | 19 +++++++++---------- lib/src/clixon_json.c | 25 ++++++++++++++++++------- test/test_choice.sh | 9 ++++----- 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06601867..6dddc838 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,10 @@ * JSON changes * Non-pretty-print output removed all extra spaces. * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` - * Empty JSON container changed from `null` to `{}`. - * Empty list and leafs remain as `null` + * Empty JSON values changed from `null` to: + * Empty yang container encoded as `{}` + * Empty leaf/leaf-list of type empty encoded as `[null]` + * Other empty values remain as `null` ### Minor changes * Removed unnecessary configure dependencies diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index 1fb8ad10..60038c1a 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -187,18 +187,17 @@ api_data_get2(clicon_handle h, } else{ #if 0 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) - goto done; - clicon_debug(1, "%s xret: %s", - __FUNCTION__, cbuf_get(ccc)); - cbuf_free(ccc); + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) + goto done; + clicon_debug(1, "%s xret: %s", + __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); } #endif - - if (xml2json_cbuf(cbx, xret, pretty) < 0) - goto done; + if (xml2json_cbuf(cbx, xret, pretty) < 0) + goto done; } } else{ diff --git a/lib/src/clixon_json.c b/lib/src/clixon_json.c index 23c9389b..a89a4162 100644 --- a/lib/src/clixon_json.c +++ b/lib/src/clixon_json.c @@ -193,14 +193,14 @@ array_eval(cxobj *xprev, char *ns2; nsx = xml_find_type_value(x, NULL, "xmlns", CX_ATTR); - if (xml_type(x)!=CX_ELMNT){ + if (xml_type(x) != CX_ELMNT){ array=BODY_ARRAY; goto done; } ys = xml_spec(x); if (xnext && xml_type(xnext)==CX_ELMNT && - strcmp(xml_name(x),xml_name(xnext))==0){ + strcmp(xml_name(x), xml_name(xnext))==0){ ns2 = xml_find_type_value(xnext, NULL, "xmlns", CX_ATTR); if ((!nsx && !ns2) || (nsx && ns2 && strcmp(nsx,ns2)==0)) @@ -420,13 +420,17 @@ json2xml_decode(cxobj *x, if (keyword == Y_LEAF || keyword == Y_LEAF_LIST){ if (yang_type_get(y, NULL, &ytype, NULL, NULL, NULL, NULL, NULL) < 0) goto done; - if (ytype) + + if (ytype){ if (strcmp(yang_argument_get(ytype),"identityref")==0){ if ((ret = json2xml_decode_identityref(x, y, xerr)) < 0) goto done; if (ret == 0) goto fail; } + else if (strcmp(yang_argument_get(ytype), "empty")==0) + ; /* dont need to do anything */ + } } } xc = NULL; @@ -464,6 +468,7 @@ xml2json_encode_identityref(cxobj *xb, yang_stmt *yspec; yang_stmt *my_ymod; + clicon_debug(1, "%s %s", __FUNCTION__, body); my_ymod = ys_module(yp); yspec = ys_spec(yp); if (nodeid_split(body, &prefix, &id) < 0) @@ -667,7 +672,6 @@ xml2json1_cbuf(cbuf *cb, modname0 = modname; /* modname0 is ancestor ns passed to child */ } childt = child_type(x); - if (pretty==2) cprintf(cb, "#%s_array, %s_child ", arraytype2str(arraytype), @@ -687,12 +691,18 @@ xml2json1_cbuf(cbuf *cb, switch (childt){ case NULL_CHILD: /* If x is a container, use {} instead of null - * That is, x is not a list or leaf + * if leaf or leaf-list then assume EMPTY type, then [null] + * else null */ if (ys && yang_keyword_get(ys) == Y_CONTAINER) cprintf(cb, "{}"); - else - cprintf(cb, "null"); + else{ + if (ys && + (yang_keyword_get(ys) == Y_LEAF || yang_keyword_get(ys) == Y_LEAF_LIST)) + cprintf(cb, "[null]"); + else + cprintf(cb, "null"); + } break; case BODY_CHILD: break; @@ -756,6 +766,7 @@ xml2json1_cbuf(cbuf *cb, xc = xml_child_i(x, i); if (xml_type(xc) == CX_ATTR) continue; /* XXX Only xmlns attributes mapped */ + xc_arraytype = array_eval(i?xml_child_i(x,i-1):NULL, xc, xml_child_i(x, i+1)); diff --git a/test/test_choice.sh b/test/test_choice.sh index 5b0d9479..5ead0d93 100755 --- a/test/test_choice.sh +++ b/test/test_choice.sh @@ -127,7 +127,6 @@ new "waiting" wait_backend wait_restconf - # First vanilla (protocol) case new "netconf validate empty" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" @@ -171,17 +170,17 @@ new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new "restconf set protocol tcp+udp fail" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": null, "udp": null}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' +expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' new "restconf set protocol tcp" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d {\"system:protocol\":{\"tcp\":null}})" 0 "" +expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d {\"system:protocol\":{\"tcp\":[null]}})" 0 "" new "restconf get protocol tcp" -expecteq "$(curl -s -X GET http://localhost/restconf/data/system:system)" 0 '{"system:system":{"protocol":{"tcp":null}}} +expecteq "$(curl -s -X GET http://localhost/restconf/data/system:system)" 0 '{"system:system":{"protocol":{"tcp":[null]}}} ' new "restconf set protocol tcp+udp fail" -expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": null, "udp": null}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' +expecteq "$(curl -s -X PUT http://localhost/restconf/data/system:system/protocol -d '{"system:protocol":{"tcp": [null], "udp": [null]}}')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"bad-element","error-info":{"bad-element":"udp"},"error-severity":"error","error-message":"Element in choice statement already exists"}}} ' new "cli set protocol udp" expectfn "$clixon_cli -1 -f $cfg -l o set system protocol udp" 0 "^$" From 2d9d204f6970b170282d6cced300ad765a90052f Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 31 Jul 2019 14:04:27 +0200 Subject: [PATCH 09/10] * RESTCONF "insert" and "point" query parameters supported * Yang Netconf leaf/leaf-list insert support * For "ordered-by user" leafs and leaf-lists, the insert and value/key attributes are supported according to RFC7950 Sections 7.7.9 and 7.8.6 * Fixed RESTCONF api-path leaf-list selection was not made properly --- CHANGELOG.md | 5 + README.md | 9 +- apps/restconf/restconf_lib.c | 94 +++++++++++ apps/restconf/restconf_lib.h | 2 + apps/restconf/restconf_main.c | 2 +- apps/restconf/restconf_methods.c | 4 +- apps/restconf/restconf_methods_post.c | 28 +++- apps/restconf/restconf_stream.c | 2 +- lib/clixon/clixon_xml.h | 12 +- lib/clixon/clixon_xml_map.h | 1 - lib/clixon/clixon_xml_sort.h | 2 +- lib/clixon/clixon_yang.h | 14 +- lib/src/clixon_datastore_write.c | 214 +++++++++++++++++++++++--- lib/src/clixon_xml.c | 52 +++++++ lib/src/clixon_xml_map.c | 46 ++++-- lib/src/clixon_xml_sort.c | 173 ++++++++++++++++----- test/test_order.sh | 103 +++++++++++++ test/test_restconf.sh | 6 +- test/test_restconf_jukebox.sh | 74 ++++++++- util/clixon_util_insert.c | 2 +- 20 files changed, 740 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dddc838..0ea76a9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Major New features * Restconf RFC 8040 increased feature compliance + * RESTCONF "insert" and "point" query parameters supported * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. @@ -14,6 +15,8 @@ * HTTP `Location:` fields added in RESTCONF POST replies * HTTP `Cache-Control: no-cache` fields added in HTTP responses (RFC Section 5.5) * Restconf monitoring capabilities (RFC Section 9.1) +* Yang Netconf leaf/leaf-list insert support + * For "ordered-by user" leafs and leaf-lists, the insert and value/key attributes are supported according to RFC7950 Sections 7.7.9 and 7.8.6 * Yang extensions support * New plugin callback: ca_extension * The main example explains how to implement a Yang extension in a backend plugin. @@ -38,6 +41,8 @@ * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs +* Fixed RESTCONF api-path leaf-list selection was not made properly + * Requesting eg `mod:x/y=42` returned the whole list: `{"y":[41,42,43]}` whereas it should only return one element: `{"y":42}` * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * Yang Unique statements with multiple schema identifiers did not work on some platforms due to memory error. diff --git a/README.md b/README.md index 9fb894e8..fac2ee1e 100644 --- a/README.md +++ b/README.md @@ -235,15 +235,16 @@ Clixon Restconf is a daemon based on FastCGI C-API. Instructions are available t run with NGINX. The implementatation is based on [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040). -The following features are supported: +The following features of RFC8040 are supported: - OPTIONS, HEAD, GET, POST, PUT, DELETE -- stream notifications (RFC8040 sec 6) -- query parameters start-time and stop-time(RFC8040 section 4.9) +- stream notifications (Sec 6) +- query parameters: "insert", "point", "start-time" and "stop-time". +- Monitoring (Sec 9) The following features are not implemented: - ETag/Last-Modified - PATCH -- query parameters other than start/stop-time. +- Query parameters: "content", "depth", "fields", "filter", "with-defaults" See [more detailed instructions](apps/restconf/README.md). diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index e4d507f3..7f28986d 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -305,6 +305,7 @@ printparam(FCGX_Request *r, /*! Print all FCGI headers * @param[in] r Fastcgi request handle + * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https */ int restconf_test(FCGX_Request *r, @@ -552,3 +553,96 @@ restconf_terminate(clicon_handle h) return 0; } +/*! If restconf insert/point attributes are present, translate to netconf + * @param[in] xdata URI->XML to translate + * @param[in] qvec Query parameters (eg where insert/point should be) + * @retval 0 OK + * @retval -1 Error + */ +int +restconf_insert_attributes(cxobj *xdata, + cvec *qvec) +{ + int retval = -1; + cxobj *xa; + char *instr; + char *pstr; + yang_stmt *y; + char *attrname; + int ret; + + y = xml_spec(xdata); + if ((instr = cvec_find_str(qvec, "insert")) != NULL){ + /* First add xmlns:yang attribute */ + if ((xa = xml_new("yang", xdata, NULL)) == NULL) + goto done; + if (xml_prefix_set(xa, "xmlns") < 0) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_value_set(xa, "urn:ietf:params:xml:ns:yang:1") < 0) + goto done; + /* Then add insert attribute */ + if ((xa = xml_new("insert", xdata, NULL)) == NULL) + goto done; + if (xml_prefix_set(xa, "yang") < 0) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_value_set(xa, instr) < 0) + goto done; + } + if ((pstr = cvec_find_str(qvec, "point")) != NULL){ + char *xpath = NULL; + char *namespace = NULL; + cbuf *cb = NULL; + if (y == NULL){ + clicon_err(OE_YANG, 0, "Cannot yang resolve %s", xml_name(xdata)); + goto done; + } + if (yang_keyword_get(y) == Y_LIST) + attrname="key"; + else + attrname="value"; + /* Then add value/key attribute */ + if ((xa = xml_new(attrname, xdata, NULL)) == NULL) + goto done; + if (xml_prefix_set(xa, "yang") < 0) + goto done; + xml_type_set(xa, CX_ATTR); + if ((ret = api_path2xpath(pstr, ys_spec(y), &xpath, &namespace)) < 0) + goto done; + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(cb, "/%s", xpath); /* XXX: also prefix/namespace? */ + if (xml_value_set(xa, cbuf_get(cb)) < 0) + goto done; + if (xpath) + free(xpath); + if (cb) + cbuf_free(cb); + } + retval = 0; + done: + return retval; +} + +/*! Extract uri-encoded uri-path from fastcgi parameters + * Use REQUEST_URI parameter and strip ?args + * REQUEST_URI have args and is encoded + * eg /interface=eth%2f0%2f0?insert=first + * DOCUMENT_URI dont have args and is not encoded + * eg /interface=eth/0/0 + * causes problems with eg /interface=eth%2f0%2f0 + */ +char * +restconf_uripath(FCGX_Request *r) +{ + char *path; + char *q; + + path = FCGX_GetParam("REQUEST_URI", r->envp); + if ((q = index(path, '?')) != NULL) + *q = '\0'; + return path; +} diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 7270807e..6fe968e1 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -63,5 +63,7 @@ int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int pretty, int use_xml, int code); int http_location(FCGX_Request *r, cxobj *xobj); int restconf_terminate(clicon_handle h); +int restconf_insert_attributes(cxobj *xdata, cvec *qvec); +char *restconf_uripath(FCGX_Request *r); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 5a7da6b7..6f495f89 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -345,7 +345,7 @@ api_restconf(clicon_handle h, cxobj *xerr; clicon_debug(1, "%s", __FUNCTION__); - path = FCGX_GetParam("REQUEST_URI", r->envp); + path = restconf_uripath(r); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); /* get xml/json in put and output */ diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index ed30ddda..0657920b 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -418,7 +418,6 @@ api_data_put(clicon_handle h, op = OP_CREATE; if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; - /* Top-of tree, no api-path * Replace xparent with x, ie bottom of api-path with data */ @@ -522,6 +521,9 @@ api_data_put(clicon_handle h, goto ok; } } + /* If restconf insert/point attributes are present, translate to netconf */ + if (restconf_insert_attributes(xdata, qvec) < 0) + goto done; /* If we already have that default namespace, remove it in child */ if ((xa = xml_find_type(xdata, NULL, "xmlns", CX_ATTR)) != NULL){ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index e50b25ec..265441b5 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -132,9 +132,8 @@ api_data_post(clicon_handle h, int nullspec = 0; int ret; - clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"", - __FUNCTION__, - api_path, data); + clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path); + clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; @@ -164,6 +163,15 @@ api_data_post(clicon_handle h, goto ok; } } +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) + goto done; + clicon_debug(1, "%s XURI:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif /* Parse input data as json or xml into xml */ if (parse_xml){ if (xml_parse_string(data, NULL, &xdata0) < 0){ @@ -224,6 +232,7 @@ api_data_post(clicon_handle h, goto ok; } xdata = xml_child_i(xdata0,0); + /* If the api-path (above) defines a module, then xdata must have a prefix * and it match the module defined in api-path. * In a POST, maybe there are cornercases where xdata (which is a child) and @@ -274,6 +283,19 @@ api_data_post(clicon_handle h, } } + /* If restconf insert/point attributes are present, translate to netconf */ + if (restconf_insert_attributes(xdata, qvec) < 0) + goto done; +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) + goto done; + clicon_debug(1, "%s XDATA:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* Create text buffer for transfer to backend */ if ((cbx = cbuf_new()) == NULL){ clicon_err(OE_UNIX, 0, "cbuf_new"); diff --git a/apps/restconf/restconf_stream.c b/apps/restconf/restconf_stream.c index 3b33d506..92d0b9c2 100644 --- a/apps/restconf/restconf_stream.c +++ b/apps/restconf/restconf_stream.c @@ -366,7 +366,7 @@ api_stream(clicon_handle h, #endif clicon_debug(1, "%s", __FUNCTION__); - path = FCGX_GetParam("DOCUMENT_URI", r->envp); + path = restconf_uripath(r); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); restconf_test(r, 1); diff --git a/lib/clixon/clixon_xml.h b/lib/clixon/clixon_xml.h index 15a181ba..caf6e424 100644 --- a/lib/clixon/clixon_xml.h +++ b/lib/clixon/clixon_xml.h @@ -51,7 +51,7 @@ * Types */ /* Netconf operation type */ -enum operation_type{ /* edit-configo */ +enum operation_type{ /* edit-config operation */ OP_MERGE, /* merge config-data */ OP_REPLACE,/* replace or create config-data */ OP_CREATE, /* create config data, error if exist */ @@ -60,6 +60,14 @@ enum operation_type{ /* edit-configo */ OP_NONE }; +/* Netconf insert type (see RFC7950 Sec 7.8.6) */ +enum insert_type{ /* edit-config insert */ + INS_FIRST, + INS_LAST, + INS_BEFORE, + INS_AFTER, +}; + enum cxobj_type {CX_ERROR=-1, CX_ELMNT, CX_ATTR, @@ -115,6 +123,7 @@ int xml_child_nr_notype(cxobj *xn, enum cxobj_type type); cxobj *xml_child_i(cxobj *xn, int i); cxobj *xml_child_i_type(cxobj *xn, int i, enum cxobj_type type); cxobj *xml_child_i_set(cxobj *xt, int i, cxobj *xc); +int xml_child_order(cxobj *xn, cxobj *xc); cxobj *xml_child_each(cxobj *xparent, cxobj *xprev, enum cxobj_type type); int xml_child_insert_pos(cxobj *x, cxobj *xc, int i); @@ -178,6 +187,7 @@ int xml_body_int32(cxobj *xb, int32_t *val); int xml_body_uint32(cxobj *xb, uint32_t *val); int xml_operation(char *opstr, enum operation_type *op); char *xml_operation2str(enum operation_type op); +int xml_attr_insert2val(char *instr, enum insert_type *ins); #if defined(__GNUC__) && __GNUC__ >= 3 int clicon_log_xml(int level, cxobj *x, char *format, ...) __attribute__ ((format (printf, 3, 4))); #else diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index d8a9423e..e3b98d69 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -56,7 +56,6 @@ int xml_yang_validate_all(clicon_handle h, cxobj *xt, cxobj **xret); int xml_yang_validate_all_top(clicon_handle h, cxobj *xt, cxobj **xret); int xml2cvec(cxobj *xt, yang_stmt *ys, cvec **cvv0); int cvec2xml_1(cvec *cvv, char *toptag, cxobj *xp, cxobj **xt0); - int xml_diff(yang_stmt *yspec, cxobj *x0, cxobj *x1, cxobj ***first, size_t *firstlen, cxobj ***second, size_t *secondlen, diff --git a/lib/clixon/clixon_xml_sort.h b/lib/clixon/clixon_xml_sort.h index a75c1358..4c74cad7 100644 --- a/lib/clixon/clixon_xml_sort.h +++ b/lib/clixon/clixon_xml_sort.h @@ -42,7 +42,7 @@ int xml_child_spec(cxobj *x, cxobj *xp, yang_stmt *yspec, yang_stmt **yp); int xml_cmp(cxobj *x1, cxobj *x2, int enm); int xml_sort(cxobj *x0, void *arg); -int xml_insert(cxobj *xp, cxobj *xc); +int xml_insert(cxobj *xp, cxobj *xc, enum insert_type ins, char *key_val); int xml_sort_verify(cxobj *x, void *arg); int match_base_child(cxobj *x0, cxobj *x1c, yang_stmt *yc, cxobj **x0cp); diff --git a/lib/clixon/clixon_yang.h b/lib/clixon/clixon_yang.h index a0bd8498..410c79a1 100644 --- a/lib/clixon/clixon_yang.h +++ b/lib/clixon/clixon_yang.h @@ -62,7 +62,7 @@ enum rfc_6020{ Y_BELONGS_TO, Y_BIT, Y_CASE, - Y_CHOICE, + Y_CHOICE, /* 10 */ Y_CONFIG, Y_CONTACT, Y_CONTAINER, @@ -72,7 +72,7 @@ enum rfc_6020{ Y_DEVIATION, Y_ENUM, Y_ERROR_APP_TAG, - Y_ERROR_MESSAGE, + Y_ERROR_MESSAGE, /* 20 */ Y_EXTENSION, Y_FEATURE, Y_FRACTION_DIGITS, @@ -82,17 +82,17 @@ enum rfc_6020{ Y_IMPORT, Y_INCLUDE, Y_INPUT, - Y_KEY, + Y_KEY, /* 30 */ Y_LEAF, Y_LEAF_LIST, Y_LENGTH, - Y_LIST, + Y_LIST, Y_MANDATORY, Y_MAX_ELEMENTS, Y_MIN_ELEMENTS, Y_MODIFIER, Y_MODULE, - Y_MUST, + Y_MUST, /* 40 */ Y_NAMESPACE, Y_NOTIFICATION, Y_ORDERED_BY, @@ -102,7 +102,7 @@ enum rfc_6020{ Y_PATTERN, Y_POSITION, Y_PREFIX, - Y_PRESENCE, + Y_PRESENCE, /* 50 */ Y_RANGE, Y_REFERENCE, Y_REFINE, @@ -112,7 +112,7 @@ enum rfc_6020{ Y_RPC, Y_STATUS, Y_SUBMODULE, - Y_TYPE, + Y_TYPE, /* 60 */ Y_TYPEDEF, Y_UNIQUE, Y_UNITS, diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index 7986b50d..9adeff20 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -122,6 +122,55 @@ replace_xmlns(cxobj *x0, return retval; } +/*! Given an attribute name and its expected namespace, find its value + * + * An attribute may have a prefix(or NULL). The routine finds the associated + * xmlns binding to find the namespace: :. + * If such an attribute is not found, failure is returned with cbret set, + * If such an attribute its found, its string value is returned. + * @param[in] x XML node (where to look for attribute) + * @param[in] name Attribute name + * @param[in] namespace (Expected)Namespace of attribute + * @param[out] cbret Error message (if retval=0) + * @param[out] valp Pointer to value (if retval=1) + * @retval -1 Error + * @retval 0 Failed (cbret set) + * @retval 1 OK + */ +static int +attr_ns_value(cxobj *x, + char *name, + char *namespace, + cbuf *cbret, + char **valp) +{ + int retval = -1; + cxobj *xa; + char *ans = NULL; /* attribute namespace */ + char *val = NULL; + + /* prefix=NULL since we do not know the prefix */ + if ((xa = xml_find_type(x, NULL, name, CX_ATTR)) != NULL){ + if (xml2ns(xa, xml_prefix(xa), &ans) < 0) + goto done; + if (ans == NULL){ /* the attribute exists, but no namespace */ + if (netconf_bad_attribute(cbret, "application", name, "Unresolved attribute prefix (no namespace?)") < 0) + goto done; + goto fail; + } + /* the attribute exists, but not w expected namespace */ + if (strcmp(ans, namespace) == 0) + val = xml_value(xa); + } + *valp = val; + retval = 1; + done: + return retval; + fail: + retval = 0; + goto done; +} + /*! Modify a base tree x0 with x1 with yang spec y according to operation op * @param[in] th Datastore text handle * @param[in] x0 Base xml tree (can be NULL in add scenarios) @@ -138,6 +187,10 @@ replace_xmlns(cxobj *x0, * @retval 1 OK * Assume x0 and x1 are same on entry and that y is the spec * @see text_modify_top + * RFC 7950 Sec 7.7.9(leaf-list), 7.8.6(lists) + * In an "ordered-by user" list, the attributes "insert" and "key" in + * the YANG XML namespace can be used to control where + * in the list the entry is inserted. */ static int text_modify(clicon_handle h, @@ -152,7 +205,7 @@ text_modify(clicon_handle h, cbuf *cbret) { int retval = -1; - char *opstr; + char *opstr = NULL; char *x1name; char *x1cname; /* child name */ cxobj *x0a; /* attribute */ @@ -167,22 +220,66 @@ text_modify(clicon_handle h, cxobj **x0vec = NULL; int i; int ret; + char *instr = NULL; + char *keystr = NULL; + char *valstr = NULL; + enum insert_type insert = INS_LAST; int changed = 0; /* Only if x0p's children have changed-> sort is necessary */ - + /* Check for operations embedded in tree according to netconf */ +#ifdef notyet /* XXX breaks in test_cohoice.sh */ + if ((ret = attr_ns_value(x1, + "operation", "urn:ietf:params:xml:ns:netconf:base:1.0", + cbret, &opstr)) < 0) + goto done; + if (ret == 0) + goto fail; + if (opstr != NULL) + if (xml_operation(opstr, &op) < 0) + goto done; +#else if ((opstr = xml_find_value(x1, "operation")) != NULL) if (xml_operation(opstr, &op) < 0) goto done; +#endif + x1name = xml_name(x1); - if (yang_keyword_get(y0) == Y_LEAF_LIST || yang_keyword_get(y0) == Y_LEAF){ - /* This is a check on no further elements as a sanity check for eg - * ab - */ + if (yang_keyword_get(y0) == Y_LEAF_LIST || + yang_keyword_get(y0) == Y_LEAF){ + /* This is a check that a leaf does not have sub-elements + * such as: a b + */ if (xml_child_nr_type(x1, CX_ELMNT)){ if (netconf_unknown_element(cbret, "application", x1name, "Leaf contains sub-element") < 0) goto done; goto fail; } + /* If leaf-list and ordered-by user, then get yang:insert attribute + * See RFC 7950 Sec 7.7.9 + */ + if (yang_keyword_get(y0) == Y_LEAF_LIST && + yang_find(y0, Y_ORDERED_BY, "user") != NULL){ + if ((ret = attr_ns_value(x1, + "insert", "urn:ietf:params:xml:ns:yang:1", + cbret, &instr)) < 0) + goto done; + if (ret == 0) + goto fail; + if (instr != NULL && + xml_attr_insert2val(instr, &insert) < 0) + goto done; + if ((ret = attr_ns_value(x1, + "value", "urn:ietf:params:xml:ns:yang:1", + cbret, &valstr)) < 0) + goto done; + /* if insert/before, value attribute must be there */ + if ((insert == INS_AFTER || insert == INS_BEFORE) && + valstr == NULL){ + if (netconf_missing_attribute(cbret, "application", "value", "Missing value attribute when insert is before or after") < 0) + goto done; + goto fail; + } + } x1bstr = xml_body(x1); switch(op){ case OP_CREATE: @@ -191,9 +288,29 @@ text_modify(clicon_handle h, goto done; goto fail; } - case OP_NONE: /* fall thru */ + case OP_REPLACE: /* fall thru */ case OP_MERGE: - case OP_REPLACE: + if (!(op == OP_MERGE && instr==NULL)){ + /* Remove existing, also applies to merge in the special case + * of ordered-by user and (changed) insert attribute. + */ + if (!permit && xnacm){ + if ((ret = nacm_datanode_write(NULL, x1, x0?NACM_UPDATE:NACM_CREATE, username, xnacm, cbret)) < 0) + goto done; + if (ret == 0) + goto fail; + permit = 1; + } + /* XXX: Note, if there is an error in adding the object later, the + * original object is not reverted. + */ + if (x0){ + xml_purge(x0); + x0 = NULL; + } + } /* OP_MERGE & insert */ + case OP_NONE: /* fall thru */ + if (x0==NULL){ if ((op != OP_NONE) && !permit && xnacm){ if ((ret = nacm_datanode_write(NULL, x1, NACM_CREATE, username, xnacm, cbret)) < 0) @@ -208,11 +325,15 @@ text_modify(clicon_handle h, goto done; changed++; - /* Copy xmlns attributes */ + /* Copy xmlns attributes ONLY, not op/insert etc */ x1a = NULL; while ((x1a = xml_child_each(x1, x1a, CX_ATTR)) != NULL) if (strcmp(xml_name(x1a),"xmlns")==0 || ((xns = xml_prefix(x1a)) && strcmp(xns, "xmlns")==0)){ +#if 1 /* XXX Kludge to NOT copy RFC7950 xmlns:yang insert/key/value namespaces */ + if (strcmp(xml_value(x1a),"urn:ietf:params:xml:ns:yang:1")==0) + continue; +#endif if ((x0a = xml_dup(x1a)) == NULL) goto done; if (xml_addsub(x0, x0a) < 0) @@ -261,8 +382,8 @@ text_modify(clicon_handle h, } } } - if (changed){ - if (xml_insert(x0p, x0) < 0) + if (changed){ + if (xml_insert(x0p, x0, insert, valstr) < 0) goto done; } break; @@ -289,6 +410,37 @@ text_modify(clicon_handle h, } /* switch op */ } /* if LEAF|LEAF_LIST */ else { /* eg Y_CONTAINER, Y_LIST, Y_ANYXML */ + /* If list and ordered-by user, then get insert attribute + + * See RFC 7950 Sec 7.8.6 + */ + if (yang_keyword_get(y0) == Y_LIST && + yang_find(y0, Y_ORDERED_BY, "user") != NULL){ + if ((ret = attr_ns_value(x1, + "insert", "urn:ietf:params:xml:ns:yang:1", + cbret, &instr)) < 0) + goto done; + if (ret == 0) + goto fail; + if (instr != NULL && + xml_attr_insert2val(instr, &insert) < 0) + goto done; + if ((ret = attr_ns_value(x1, + "key", "urn:ietf:params:xml:ns:yang:1", + cbret, &keystr)) < 0) + goto done; + /* if insert/before, key attribute must be there */ + if ((insert == INS_AFTER || insert == INS_BEFORE) && + keystr == NULL){ + if (netconf_missing_attribute(cbret, "application", "key", "Missing key attribute when insert is before or after") < 0) + goto done; + goto fail; + } + + } switch(op){ case OP_CREATE: if (x0){ @@ -297,19 +449,27 @@ text_modify(clicon_handle h, goto fail; } case OP_REPLACE: /* fall thru */ - if (!permit && xnacm){ - if ((ret = nacm_datanode_write(NULL, x1, x0?NACM_UPDATE:NACM_CREATE, username, xnacm, cbret)) < 0) - goto done; - if (ret == 0) - goto fail; - permit = 1; - } - if (x0){ - xml_purge(x0); - x0 = NULL; - } - case OP_MERGE: /* fall thru */ - case OP_NONE: + case OP_MERGE: + if (!(op == OP_MERGE && instr==NULL)){ + /* Remove existing, also applies to merge in the special case + * of ordered-by user and (changed) insert attribute. + */ + if (!permit && xnacm){ + if ((ret = nacm_datanode_write(NULL, x1, x0?NACM_UPDATE:NACM_CREATE, username, xnacm, cbret)) < 0) + goto done; + if (ret == 0) + goto fail; + permit = 1; + } + /* XXX: Note, if there is an error in adding the object later, the + * original object is not reverted. + */ + if (x0){ + xml_purge(x0); + x0 = NULL; + } + } /* OP_MERGE & insert */ + case OP_NONE: /* fall thru */ /* Special case: anyxml, just replace tree, See rfc6020 7.10.3:n An anyxml node is treated as an opaque chunk of data. This data @@ -356,6 +516,10 @@ text_modify(clicon_handle h, while ((x1a = xml_child_each(x1, x1a, CX_ATTR)) != NULL) if (strcmp(xml_name(x1a),"xmlns")==0 || ((xns = xml_prefix(x1a)) && strcmp(xns, "xmlns")==0)){ +#if 1 /* XXX Kludge to NOT copy RFC7950 xmlns:yang insert/key/value namespaces */ + if (strcmp(xml_value(x1a),"urn:ietf:params:xml:ns:yang:1")==0) + continue; +#endif if ((x0a = xml_dup(x1a)) == NULL) goto done; if (xml_addsub(x0, x0a) < 0) @@ -412,7 +576,7 @@ text_modify(clicon_handle h, goto fail; } if (changed){ - if (xml_insert(x0p, x0) < 0) + if (xml_insert(x0p, x0, insert, keystr) < 0) goto done; } break; diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index e7d7e371..ccb31ab6 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -625,6 +625,7 @@ xml_child_nr_type(cxobj *xn, * @retval xml The child xml node * @retval NULL if no such child, or empty child * @see xml_child_i_type + * @see xml_child_order */ cxobj * xml_child_i(cxobj *xn, @@ -673,6 +674,29 @@ xml_child_i_set(cxobj *xt, return 0; } +/*! Get the order of child + * @param[in] xp xml parent node + * @param[in] xc the xml child to look for + * @retval xml The child xml node + * @retval i The order of the child + * @retval -1 if no such child, or empty child + * @see xml_child_i + */ +int +xml_child_order(cxobj *xp, + cxobj *xc) +{ + cxobj *x = NULL; + int i = 0; + + while ((x = xml_child_each(xp, x, -1)) != NULL) { + if (x == xc) + return i; + i++; + } + return -1; +} + /*! Iterator over xml children objects * * @note Never manipulate the child-list during operation or using the @@ -2375,6 +2399,8 @@ xml_operation(char *opstr, return 0; } + + /*! Map xml operation from enumeration to string * @param[in] op enumeration operation, eg OP_MERGE,... * @retval str String, eg "merge". Static string, no free necessary @@ -2406,6 +2432,32 @@ xml_operation2str(enum operation_type op) return "none"; } } +/*! Map xml insert attribute from string to enumeration + * @param[in] instr String, eg "first" + * @param[out] ins Enumeration, eg INS_FIRST + * @code + * enum insert_type ins; + * xml_operation("last", &ins) + * @endcode + */ +int +xml_attr_insert2val(char *instr, + enum insert_type *ins) +{ + if (strcmp("first", instr) == 0) + *ins = INS_FIRST; + else if (strcmp("last", instr) == 0) + *ins = INS_LAST; + else if (strcmp("before", instr) == 0) + *ins = INS_BEFORE; + else if (strcmp("after", instr) == 0) + *ins = INS_AFTER; + else{ + clicon_err(OE_XML, 0, "Bad-attribute operation: %s", instr); + return -1; + } + return 0; +} /*! Specialization of clicon_debug with xml tree * @param[in] level log level, eg LOG_DEBUG,LOG_INFO,...,LOG_EMERG. diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index afb18f09..3e395a4d 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -2198,7 +2198,7 @@ xml_default(cxobj *xt, goto done; free(str); added++; - if (xml_insert(xt, xc) < 0) + if (xml_insert(xt, xc, INS_LAST, NULL) < 0) goto done; } } @@ -2455,21 +2455,33 @@ api_path2xpath_cvv(cvec *api_path, if (cv2str(cv, NULL, 0) > 0){ if ((val = cv2str_dup(cv)) == NULL) goto done; - v = val; - /* XXX sync with yang */ - while((v=index(v, ',')) != NULL){ - *v = '\0'; - v++; - } - cvk = y->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ - cvi = NULL; - /* Iterate over individual yang keys */ - - cprintf(xpath, "/%s", name); - v = val; - while ((cvi = cvec_each(cvk, cvi)) != NULL){ - cprintf(xpath, "[%s='%s']", cv_string_get(cvi), v); - v += strlen(v)+1; + switch (yang_keyword_get(y)){ + case Y_LIST: + v = val; + while((v=index(v, ',')) != NULL){ + *v = '\0'; + v++; + } + cvk = y->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ + cvi = NULL; + /* Iterate over individual yang keys */ + cprintf(xpath, "/%s", name); + v = val; + while ((cvi = cvec_each(cvk, cvi)) != NULL){ + cprintf(xpath, "[%s='%s']", cv_string_get(cvi), v); + v += strlen(v)+1; + } + break; + case Y_LEAF_LIST: /* XXX: LOOP? */ + cprintf(xpath, "/%s", name); + if (val) + cprintf(xpath, "[.='%s']", val); + else + cprintf(xpath, "[.='']"); + break; + default: + cprintf(xpath, "%s%s", (i==offset?"":"/"), name); + break; } if (val) free(val); @@ -2516,7 +2528,7 @@ api_path2xpath_cvv(cvec *api_path, * ... access xpath as cbuf_get(xpath) * free(xpath) * @endcode - + * * @see api_path2xml_cvv which uses other parameter formats */ int diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index 30a1f332..ba9b8435 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -61,6 +61,8 @@ #include "clixon_handle.h" #include "clixon_yang.h" #include "clixon_xml.h" +#include "clixon_xpath_ctx.h" +#include "clixon_xpath.h" #include "clixon_options.h" #include "clixon_xml_map.h" #include "clixon_yang_type.h" @@ -361,30 +363,35 @@ xml_sort(cxobj *x, } /*! Special case search for ordered-by user where linear sort is used + * @param[in] xp Parent XML node (go through its childre) + * @param[in] x1 XML node to match + * @param[in] yangi Yang order number (according to spec) + * @param[in] mid Where to start from (may be in middle of interval) + * @retval NULL Not found + * @retval x XML element that matches x1 */ static cxobj * xml_search_userorder(cxobj *xp, cxobj *x1, - yang_stmt *y, int yangi, int mid) - { - int i; - cxobj *xc; + int i; + cxobj *xc; + yang_stmt *yc; for (i=mid+1; i=0; i--){ /* Then decrement */ xc = xml_child_i(xp, i); - y = xml_spec(xc); - if (yangi!=yang_order(y)) + yc = xml_spec(xc); + if (yangi!=yang_order(yc)) break; if (xml_cmp(xc, x1, 0) == 0) return xc; @@ -427,7 +434,7 @@ xml_search1(cxobj *xp, if (cmp == 0){ cmp = xml_cmp(x1, xc, 0); if (cmp && userorder) /* Ordered by user (if not equal) */ - return xml_search_userorder(xp, x1, y, yangi, mid); + return xml_search_userorder(xp, x1, yangi, mid); } if (cmp == 0) return xc; @@ -472,25 +479,123 @@ xml_search(cxobj *xp, return xret; } +/*! Insert xn in xp:s sorted child list (special case of ordered-by user) + * @param[in] xp Parent xml node. If NULL just remove from old parent. + * @param[in] xn Child xml node to insert under xp + * @param[in] yn Yang stmt of xml child node + * @param[in] ins Insert operation (if ordered-by user) + * @param[in] key_val Key if LIST and ins is before/after, val if LEAF_LIST + * @retval i Order where xn should be inserted into xp:s children + * @retval -1 Error + */ + +static int +xml_insert_userorder(cxobj *xp, + cxobj *xn, + yang_stmt *yn, + int mid, + enum insert_type ins, + char *key_val) +{ + int retval = -1; + int i; + cxobj *xc; + yang_stmt *yc; + char *kludge = ""; /* Cant get instance-id generic of [.. and /.. case */ + + switch (ins){ + case INS_FIRST: + for (i=mid-1; i>=0; i--){ /* decrement */ + xc = xml_child_i(xp, i); + yc = xml_spec(xc); + if (yc != yn){ + retval = i+1; + goto done; + } + } + retval = i+1; + break; + case INS_LAST: + for (i=mid+1; i upper){ /* beyond range */ clicon_err(OE_XML, 0, "low>upper %d %d", low, upper); @@ -512,15 +617,7 @@ xml_insert2(cxobj *xp, } if (yc == yn){ /* Same yang */ if (userorder){ /* append: increment linearly until no longer equal */ - for (i=mid+1; i]]>]]>' '^1.02.010.0]]>]]>$' +new "delete candidate" +expecteof "$clixon_netconf -qf $cfg" 0 'none]]>]]>' "^]]>]]>$" + +new "netconf commit" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" + +# leaf-list ordered-by-user, "insert" and "value" attributes +# y0 is leaf-list ordered by user +new "add one entry (c) to leaf-list" +expecteof "$clixon_netconf -qf $cfg" 0 'c]]>]]>' "^]]>]]>$" + +new "add one entry (a) to leaf-list first (with no yang namespace - error)" +expecteof "$clixon_netconf -qf $cfg" 0 'a]]>]]>' '^applicationbad-attributeinserterrorUnresolved attribute prefix (no namespace?)]]>]]>$' + +new "add one entry (b) to leaf-list first" +expecteof "$clixon_netconf -qf $cfg" 0 'b]]>]]>' "^]]>]]>$" + +new "add one entry (d) to leaf-list last" +expecteof "$clixon_netconf -qf $cfg" 0 'd]]>]]>' "^]]>]]>$" + +new "add one entry (a) to leaf-list first" +expecteof "$clixon_netconf -qf $cfg" 0 'a]]>]]>' "^]]>]]>$" + +new "add one entry (e) to leaf-list last" +expecteof "$clixon_netconf -qf $cfg" 0 'e]]>]]>' "^]]>]]>$" + +new "check ordered-by-user: a,b,c,d,e" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^abcde]]>]]>$' + +new "move one entry (e) to leaf-list first" +expecteof "$clixon_netconf -qf $cfg" 0 'e]]>]]>' "^]]>]]>$" + +new "check ordered-by-user: e,a,b,c,d" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^eabcd]]>]]>$' + +# before and after and value attribute +new "add one leaf-list entry 71 before b" +XML="71]]>]]>" +expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "^]]>]]>$" + +new "add one entry 42 after b" +XML="42]]>]]>" +expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "^]]>]]>$" + +# XXX actually not right error message, should be as RFC7950 Sec 15.7 +new "add one entry 99 after Q (not found, error)" +XML="99]]>]]>" +RES="^protocoloperation-failederrorbad-attribute: value, missing-instance: y0\[.='Q'\]]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 "$XML" "$RES" + +new "check ordered-by-user: e,a,71,b,42,c,d" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^ea71b42cd]]>]]>$' + +new "delete candidate" +expecteof "$clixon_netconf -qf $cfg" 0 'none]]>]]>' "^]]>]]>$" + +new "netconf commit" +expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" + +# list ordered-by-user, "insert" and "value" attributes +# y2 is list ordered by user +new "add one entry (key c) to list" +expecteof "$clixon_netconf -qf $cfg" 0 'cfoo]]>]]>' "^]]>]]>$" + +new "add one entry (key a) to list first (with no yang namespace - error)" +expecteof "$clixon_netconf -qf $cfg" 0 'afoo]]>]]>' '^applicationbad-attributeinserterrorUnresolved attribute prefix (no namespace?)]]>]]>$' + +new "add one entry (key b) to list first" +expecteof "$clixon_netconf -qf $cfg" 0 'bbar]]>]]>' "^]]>]]>$" + +new "add one entry (d) to list last" +expecteof "$clixon_netconf -qf $cfg" 0 'dfie]]>]]>' "^]]>]]>$" + +new "add one entry (a) to list first" +expecteof "$clixon_netconf -qf $cfg" 0 'afoo]]>]]>' "^]]>]]>$" + +new "add one entry (e) to list last" +expecteof "$clixon_netconf -qf $cfg" 0 'ebar]]>]]>' "^]]>]]>$" + +new "check ordered-by-user: a,b,c,d,e" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^afoobbarcfoodfieebar]]>]]>$' + +new "move one entry (e) to list first" +expecteof "$clixon_netconf -qf $cfg" 0 'ebar]]>]]>' "^]]>]]>$" + +new "check ordered-by-user: e,a,b,c,d" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^ebarafoobbarcfoodfie]]>]]>$' + +# before and after and key attribute +new "add one entry 71 before key b" +expecteof "$clixon_netconf -qf $cfg" 0 "71fie]]>]]>" "^]]>]]>$" + +new "add one entry 42 after key b" +expecteof "$clixon_netconf -qf $cfg" 0 "42fum]]>]]>" "^]]>]]>$" + +# XXX actually not right error message, should be as RFC7950 Sec 15.7 +new "add one entry key 99 after Q (not found, error)" +expecteof "$clixon_netconf -qf $cfg" 0 "99bar]]>]]>" "^protocoloperation-failederrorbad-attribute: key, missing-instance: y2\[k='Q'\]]]>]]>$" + +new "check ordered-by-user: e,a,71,b,42,c,d" +expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^ebarafoo71fiebbar42fumcfoodfie]]>]]>$' + if [ $BE -eq 0 ]; then exit # BE fi diff --git a/test/test_restconf.sh b/test/test_restconf.sh index aa579de2..20ada66d 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -128,8 +128,8 @@ if [ -z "$match" ]; then err "$expect" "$ret" fi -new "restconf get data/ json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":["42","41","43"]} +new "restconf get data type json" +expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} ' new "restconf get state operation" @@ -142,7 +142,7 @@ if [ -z "$match" ]; then fi new "restconf get state operation type json" -expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":["42","41","43"]} +expecteq "$(curl -s -G http://localhost/restconf/data/clixon-example:state/op=42)" 0 '{"clixon-example:op":"42"} ' new "restconf get state operation type xml" diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index f36c96d6..702d19ce 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -282,6 +282,11 @@ cat < $fyang } } } + leaf-list extra{ + type string; + ordered-by user; + description "Extra added to test ordered-by user inserts on leaf-lists"; + } } EOF @@ -344,23 +349,84 @@ expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' htt new "4.5. PUT create new identity" expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" +new "restconf DELETE whole datastore" +expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" + +new "B.2.4. Replace a Datastore Resource" +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 "HTTP/1.1 201 Created" + +new "restconf DELETE whole datastore" +expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" + +new 'B.3.4. "insert" Parameter' +JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" + +new 'B.3.4. "insert" Parameter first (RFC example says after)' +JSON="{\"example-jukebox:song\":[{\"index\":0,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" + +new 'B.3.4. "insert" Parameter check order' +RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" + +new 'B.3.5. "point" Parameter (before for more interesting order: 0,2,1)' +JSON="{\"example-jukebox:song\":[{\"index\":2,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" + +new 'B.3.5. "point" check order (0,2,1)' +RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" + +#XXX 'Location: https://example.com/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' + +new 'B.3.5. "point" Parameter 3 after 2 (using PUT)' +JSON="{\"example-jukebox:song\":[{\"index\":3,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Something else']\"}]}" +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=3?insert=after\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D2 )" 0 "HTTP/1.1 201 Created" + +new 'B.3.5. "point" check order (0,2,3,1)' +RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]3/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Something else'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" + +new "restconf DELETE whole datastore" +expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" + +new 'B.3.4. "insert/point" leaf-list 3 (not in RFC)' +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" + +new 'B.3.4. "insert/point" leaf-list 2 first' +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" + +new 'B.3.4. "insert/point" leaf-list 1 last' +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" + +#new 'B.3.4. "insert/point" move leaf-list 1 last' +#- restconf cannot move a leaf-list(list?) item +#expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" + +new 'B.3.5. "insert/point" leaf-list check order (2,3,1)' +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' + +new 'B.3.5. "point" Parameter leaf-list 4 before 3' +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" + +new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' +expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431' if false; then # NYI new "B.2.2. Detect Datastore Resource Entity-Tag Change" new "B.2.3. Edit a Datastore Resource" -new "B.2.4. Replace a Datastore Resource" new "B.2.5. Edit a Data Resource" new 'B.3.1. "content" Parameter' new 'B.3.2. "depth" Parameter' new 'B.3.3. "fields" Parameter' -new 'B.3.4. "insert" Parameter' -new 'B.3.5. "point" Parameter' new 'B.3.6. "filter" Parameter' new 'B.3.7. "start-time" Parameter' new 'B.3.8. "stop-time" Parameter' new 'B.3.9. "with-defaults" Parameter' -fi + +fi # NYI new "Kill restconf daemon" stop_restconf diff --git a/util/clixon_util_insert.c b/util/clixon_util_insert.c index 7d6f675f..34775f3d 100644 --- a/util/clixon_util_insert.c +++ b/util/clixon_util_insert.c @@ -185,7 +185,7 @@ main(int argc, char **argv) clicon_debug(1, "xi:"); xml_print(stderr, xi); } - if (xml_insert(xb, xi) < 0) + if (xml_insert(xb, xi, INS_LAST, NULL) < 0) goto done; if (debug){ clicon_debug(1, "x0:"); From c97346921b3876771623a7f53195a8d874862b6b Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 31 Jul 2019 16:45:48 +0200 Subject: [PATCH 10/10] Netconf operation attribute namespace check is enforced --- apps/backend/backend_client.c | 26 ++++++++++---------- apps/cli/cli_common.c | 15 +++++------- apps/netconf/netconf_hello.c | 2 +- apps/restconf/restconf_lib.c | 2 +- apps/restconf/restconf_methods.c | 16 +++++++++--- apps/restconf/restconf_methods_post.c | 6 ++++- example/main/example_backend.c | 2 +- example/main/example_cli.c | 3 ++- lib/clixon/clixon_xml.h | 6 +++++ lib/src/clixon_datastore_write.c | 35 ++++++++++++--------------- lib/src/clixon_proto_client.c | 1 + test/test_copy_config.sh | 21 ++++++++-------- test/test_leafref.sh | 2 +- test/test_netconf.sh | 10 ++++---- test/test_order.sh | 2 +- test/test_perf.sh | 2 +- test/test_transaction.sh | 2 +- test/test_unique.sh | 4 +-- test/test_yang.sh | 2 +- 19 files changed, 86 insertions(+), 73 deletions(-) diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index e8437a88..07329adc 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -1377,46 +1377,46 @@ backend_rpc_init(clicon_handle h) /* In backend_client.? RFC 6241 */ if (rpc_callback_register(h, from_client_get_config, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "get-config") < 0) + NETCONF_BASE_NAMESPACE, "get-config") < 0) goto done; if (rpc_callback_register(h, from_client_edit_config, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "edit-config") < 0) + NETCONF_BASE_NAMESPACE, "edit-config") < 0) goto done; if (rpc_callback_register(h, from_client_copy_config, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "copy-config") < 0) + NETCONF_BASE_NAMESPACE, "copy-config") < 0) goto done; if (rpc_callback_register(h, from_client_delete_config, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "delete-config") < 0) + NETCONF_BASE_NAMESPACE, "delete-config") < 0) goto done; if (rpc_callback_register(h, from_client_lock, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "lock") < 0) + NETCONF_BASE_NAMESPACE, "lock") < 0) goto done; if (rpc_callback_register(h, from_client_unlock, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "unlock") < 0) + NETCONF_BASE_NAMESPACE, "unlock") < 0) goto done; if (rpc_callback_register(h, from_client_get, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "get") < 0) + NETCONF_BASE_NAMESPACE, "get") < 0) goto done; if (rpc_callback_register(h, from_client_close_session, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "close-session") < 0) + NETCONF_BASE_NAMESPACE, "close-session") < 0) goto done; if (rpc_callback_register(h, from_client_kill_session, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "kill-session") < 0) + NETCONF_BASE_NAMESPACE, "kill-session") < 0) goto done; /* In backend_commit.? */ if (rpc_callback_register(h, from_client_commit, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "commit") < 0) + NETCONF_BASE_NAMESPACE, "commit") < 0) goto done; if (rpc_callback_register(h, from_client_discard_changes, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "discard-changes") < 0) + NETCONF_BASE_NAMESPACE, "discard-changes") < 0) goto done; /* if-feature confirmed-commit */ if (rpc_callback_register(h, from_client_cancel_commit, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "cancel-commit") < 0) + NETCONF_BASE_NAMESPACE, "cancel-commit") < 0) goto done; /* if-feature validate */ if (rpc_callback_register(h, from_client_validate, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", "validate") < 0) + NETCONF_BASE_NAMESPACE, "validate") < 0) goto done; /* In backend_client.? RPC from RFC 5277 */ diff --git a/apps/cli/cli_common.c b/apps/cli/cli_common.c index 81ceb677..f0840491 100644 --- a/apps/cli/cli_common.c +++ b/apps/cli/cli_common.c @@ -236,19 +236,15 @@ cli_dbxml(clicon_handle h, enum operation_type op) { int retval = -1; - // char *str = NULL; - char *api_path_fmt; /* xml key format */ + char *api_path_fmt; /* xml key format */ char *api_path = NULL; /* xml key */ - // cg_var *cval; - // int len; cg_var *arg; cbuf *cb = NULL; yang_stmt *yspec; - cxobj *xbot = NULL; /* xpath, NULL if datastore */ - yang_stmt *y = NULL; /* yang spec of xpath */ - cxobj *xtop = NULL; /* xpath root */ - cxobj *xa; /* attribute */ - // cxobj *xb; /* body */ + cxobj *xbot = NULL; /* xpath, NULL if datastore */ + yang_stmt *y = NULL; /* yang spec of xpath */ + cxobj *xtop = NULL; /* xpath root */ + cxobj *xa; /* attribute */ if (cvec_len(argv) != 1){ clicon_err(OE_PLUGIN, 0, "Requires one element to be xml key format string"); @@ -271,6 +267,7 @@ cli_dbxml(clicon_handle h, if ((xa = xml_new("operation", xbot, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + xml_prefix_set(xa, NETCONF_BASE_PREFIX); if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; if (yang_keyword_get(y) != Y_LIST && yang_keyword_get(y) != Y_LEAF_LIST){ diff --git a/apps/netconf/netconf_hello.c b/apps/netconf/netconf_hello.c index 9c4b07ba..c7ab25ac 100644 --- a/apps/netconf/netconf_hello.c +++ b/apps/netconf/netconf_hello.c @@ -161,7 +161,7 @@ netconf_create_hello(clicon_handle h, if ((ietf_yang_library_revision = yang_modules_revision(h)) == NULL) goto done; add_preamble(cb); - cprintf(cb, ""); + cprintf(cb, "", NETCONF_BASE_NAMESPACE); cprintf(cb, ""); cprintf(cb, "urn:ietf:params:netconf:base:1.0"); if (xml_chardata_encode(&encstr, "urn:ietf:params:netconf:capability:yang-library:1.0?revision=%s&module-set-id=%s", diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 7f28986d..f29f1b25 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -579,7 +579,7 @@ restconf_insert_attributes(cxobj *xdata, if (xml_prefix_set(xa, "xmlns") < 0) goto done; xml_type_set(xa, CX_ATTR); - if (xml_value_set(xa, "urn:ietf:params:xml:ns:yang:1") < 0) + if (xml_value_set(xa, YANG_XML_NAMESPACE) < 0) goto done; /* Then add insert attribute */ if ((xa = xml_new("insert", xdata, NULL)) == NULL) diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 0657920b..e795d58a 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -290,8 +290,8 @@ api_data_put(clicon_handle h, char *dname; int nullspec = 0; - clicon_debug(1, "%s api_path:\"%s\" data:\"%s\"", - __FUNCTION__, api_path0, data); + clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path0); + clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); goto done; @@ -415,6 +415,7 @@ api_data_put(clicon_handle h, if ((xa = xml_new("operation", xdata, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + xml_prefix_set(xa, NETCONF_BASE_PREFIX); op = OP_CREATE; if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; @@ -542,7 +543,10 @@ api_data_put(clicon_handle h, */ username = clicon_username_get(h); again: - cprintf(cbx, "", username?username:""); + cprintf(cbx, "", + username?username:"", + NETCONF_BASE_PREFIX, + NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */ cprintf(cbx, ""); cprintf(cbx, "none"); if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) @@ -720,6 +724,7 @@ api_data_delete(clicon_handle h, if ((xa = xml_new("operation", xbot, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + xml_prefix_set(xa, NETCONF_BASE_PREFIX); if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; if ((cbx = cbuf_new()) == NULL) @@ -727,7 +732,10 @@ api_data_delete(clicon_handle h, /* For internal XML protocol: add username attribute for access control */ username = clicon_username_get(h); - cprintf(cbx, "", username?username:""); + cprintf(cbx, "", + username?username:"", + NETCONF_BASE_PREFIX, + NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */ cprintf(cbx, ""); cprintf(cbx, "none"); if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index 265441b5..0dfb77b6 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -259,6 +259,7 @@ api_data_post(clicon_handle h, if ((xa = xml_new("operation", xdata, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + xml_prefix_set(xa, NETCONF_BASE_PREFIX); if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; /* Replace xbot with x, ie bottom of api-path with data */ @@ -304,7 +305,10 @@ api_data_post(clicon_handle h, /* For internal XML protocol: add username attribute for access control */ username = clicon_username_get(h); - cprintf(cbx, "", username?username:""); + cprintf(cbx, "", + username?username:"", + NETCONF_BASE_PREFIX, + NETCONF_BASE_NAMESPACE); /* bind nc to netconf namespace */ cprintf(cbx, ""); cprintf(cbx, "none"); if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) diff --git a/example/main/example_backend.c b/example/main/example_backend.c index eac3e9da..647e892b 100644 --- a/example/main/example_backend.c +++ b/example/main/example_backend.c @@ -749,7 +749,7 @@ clixon_plugin_init(clicon_handle h) /* Called after the regular system copy_config callback */ if (rpc_callback_register(h, example_copy_extra, NULL, - "urn:ietf:params:xml:ns:netconf:base:1.0", + NETCONF_BASE_NAMESPACE, "copy-config" ) < 0) goto done; diff --git a/example/main/example_cli.c b/example/main/example_cli.c index adbdefba..7e1ec757 100644 --- a/example/main/example_cli.c +++ b/example/main/example_cli.c @@ -95,7 +95,8 @@ example_client_rpc(clicon_handle h, /* User supplied variable in CLI command */ cva = cvec_find(cvv, "a"); /* get a cligen variable from vector */ /* Create XML for example netconf RPC */ - if (xml_parse_va(&xtop, NULL, "%s", + if (xml_parse_va(&xtop, NULL, "%s", + NETCONF_BASE_NAMESPACE, clicon_username_get(h), cv_string_get(cva)) < 0) goto done; diff --git a/lib/clixon/clixon_xml.h b/lib/clixon/clixon_xml.h index caf6e424..b9ef6d08 100644 --- a/lib/clixon/clixon_xml.h +++ b/lib/clixon/clixon_xml.h @@ -44,9 +44,15 @@ /* Default NETCONF namespace (see rfc6241 3.1) * See USE_NETCONF_NS_AS_DEFAULT for use of this namespace as default + * Also, bind it to prefix:nc as used by, for example, the operation attribute */ #define NETCONF_BASE_NAMESPACE "urn:ietf:params:xml:ns:netconf:base:1.0" +#define NETCONF_BASE_PREFIX "nc" +/* See RFC 7950 Sec 5.3.1: YANG defines an XML namespace for NETCONF + * operations, content, and the element. + */ +#define YANG_XML_NAMESPACE "urn:ietf:params:xml:ns:yang:1" /* * Types */ diff --git a/lib/src/clixon_datastore_write.c b/lib/src/clixon_datastore_write.c index 9adeff20..b2e07595 100644 --- a/lib/src/clixon_datastore_write.c +++ b/lib/src/clixon_datastore_write.c @@ -227,9 +227,8 @@ text_modify(clicon_handle h, int changed = 0; /* Only if x0p's children have changed-> sort is necessary */ /* Check for operations embedded in tree according to netconf */ -#ifdef notyet /* XXX breaks in test_cohoice.sh */ if ((ret = attr_ns_value(x1, - "operation", "urn:ietf:params:xml:ns:netconf:base:1.0", + "operation", NETCONF_BASE_NAMESPACE, cbret, &opstr)) < 0) goto done; if (ret == 0) @@ -237,12 +236,6 @@ text_modify(clicon_handle h, if (opstr != NULL) if (xml_operation(opstr, &op) < 0) goto done; -#else - if ((opstr = xml_find_value(x1, "operation")) != NULL) - if (xml_operation(opstr, &op) < 0) - goto done; -#endif - x1name = xml_name(x1); if (yang_keyword_get(y0) == Y_LEAF_LIST || yang_keyword_get(y0) == Y_LEAF){ @@ -260,7 +253,7 @@ text_modify(clicon_handle h, if (yang_keyword_get(y0) == Y_LEAF_LIST && yang_find(y0, Y_ORDERED_BY, "user") != NULL){ if ((ret = attr_ns_value(x1, - "insert", "urn:ietf:params:xml:ns:yang:1", + "insert", YANG_XML_NAMESPACE, cbret, &instr)) < 0) goto done; if (ret == 0) @@ -269,7 +262,7 @@ text_modify(clicon_handle h, xml_attr_insert2val(instr, &insert) < 0) goto done; if ((ret = attr_ns_value(x1, - "value", "urn:ietf:params:xml:ns:yang:1", + "value", YANG_XML_NAMESPACE, cbret, &valstr)) < 0) goto done; /* if insert/before, value attribute must be there */ @@ -331,7 +324,8 @@ text_modify(clicon_handle h, if (strcmp(xml_name(x1a),"xmlns")==0 || ((xns = xml_prefix(x1a)) && strcmp(xns, "xmlns")==0)){ #if 1 /* XXX Kludge to NOT copy RFC7950 xmlns:yang insert/key/value namespaces */ - if (strcmp(xml_value(x1a),"urn:ietf:params:xml:ns:yang:1")==0) + if (strcmp(xml_value(x1a), YANG_XML_NAMESPACE)==0 || + strcmp(xml_value(x1a), NETCONF_BASE_NAMESPACE)==0) continue; #endif if ((x0a = xml_dup(x1a)) == NULL) @@ -420,7 +414,7 @@ text_modify(clicon_handle h, if (yang_keyword_get(y0) == Y_LIST && yang_find(y0, Y_ORDERED_BY, "user") != NULL){ if ((ret = attr_ns_value(x1, - "insert", "urn:ietf:params:xml:ns:yang:1", + "insert", YANG_XML_NAMESPACE, cbret, &instr)) < 0) goto done; if (ret == 0) @@ -429,7 +423,7 @@ text_modify(clicon_handle h, xml_attr_insert2val(instr, &insert) < 0) goto done; if ((ret = attr_ns_value(x1, - "key", "urn:ietf:params:xml:ns:yang:1", + "key", YANG_XML_NAMESPACE, cbret, &keystr)) < 0) goto done; /* if insert/before, key attribute must be there */ @@ -517,7 +511,8 @@ text_modify(clicon_handle h, if (strcmp(xml_name(x1a),"xmlns")==0 || ((xns = xml_prefix(x1a)) && strcmp(xns, "xmlns")==0)){ #if 1 /* XXX Kludge to NOT copy RFC7950 xmlns:yang insert/key/value namespaces */ - if (strcmp(xml_value(x1a),"urn:ietf:params:xml:ns:yang:1")==0) + if (strcmp(xml_value(x1a), YANG_XML_NAMESPACE)==0 || + strcmp(xml_value(x1a), NETCONF_BASE_NAMESPACE)==0) continue; #endif if ((x0a = xml_dup(x1a)) == NULL) @@ -650,12 +645,14 @@ text_modify_top(clicon_handle h, char *opstr; int ret; - /* Assure top-levels are 'config' */ - // assert(x0 && strcmp(xml_name(x0),"config")==0); - // assert(x1 && strcmp(xml_name(x1),"config")==0); - /* Check for operations embedded in tree according to netconf */ - if ((opstr = xml_find_value(x1, "operation")) != NULL) + if ((ret = attr_ns_value(x1, + "operation", NETCONF_BASE_NAMESPACE, + cbret, &opstr)) < 0) + goto done; + if (ret == 0) + goto fail; + if (opstr != NULL) if (xml_operation(opstr, &op) < 0) goto done; /* Special case if x1 is empty, top-level only */ diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c index 30671817..98ed0fdd 100644 --- a/lib/src/clixon_proto_client.c +++ b/lib/src/clixon_proto_client.c @@ -357,6 +357,7 @@ clicon_rpc_edit_config(clicon_handle h, if ((cb = cbuf_new()) == NULL) goto done; cprintf(cb, "<%s/>", db); diff --git a/test/test_copy_config.sh b/test/test_copy_config.sh index 75e3eea9..f29f083f 100755 --- a/test/test_copy_config.sh +++ b/test/test_copy_config.sh @@ -69,23 +69,22 @@ if [ $BE -ne 0 ]; then if [ $? -ne 0 ]; then err fi - new "start backend -s init -f $cfg" - # start new backend - echo "sudo $clixon_backend -s init -f $cfg -D $DBG" - sudo $clixon_backend -s init -f $cfg -D $DBG - if [ $? -ne 0 ]; then - err - fi + + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg + + new "waiting" + wait_backend fi new "Add config to candidate" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' "^]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' "^]]>]]>$" new "netconf commit to running" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" new "Delete candidate" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' # Here startup and candidate are empty, only running has content # test running->startup and running->candidate @@ -123,7 +122,7 @@ new "Check startup content" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^eth/0/0ex:ethtrue]]>]]>$' new "Delete candidate" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' # Here candidate is empty and startup has content # test startup->candidate @@ -138,7 +137,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' new "netconf commit to running" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^]]>]]>$" diff --git a/test/test_leafref.sh b/test/test_leafref.sh index ef8c62b5..36bfebf7 100755 --- a/test/test_leafref.sh +++ b/test/test_leafref.sh @@ -168,7 +168,7 @@ new "leafref validate (ok)" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^" new "leafref delete leaf" -expecteof "$clixon_netconf -qf $cfg" 0 'eth0]]>]]>' '^' +expecteof "$clixon_netconf -qf $cfg" 0 'eth0]]>]]>' '^' new "leafref validate (should fail)" expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" '^applicationbad-elementeth0errorLeafref validation failed: No such leaf]]>]]>$' diff --git a/test/test_netconf.sh b/test/test_netconf.sh index 1ece0926..b7b77f0a 100755 --- a/test/test_netconf.sh +++ b/test/test_netconf.sh @@ -65,7 +65,7 @@ new "Check nothing added" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' new "Add subtree eth/0/0 using none and create which should add eth/0/0" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' "^]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' "^]]>]]>$" # Too many quotes, (single inside double inside single) need to fool bash cat < $tmp # new @@ -76,19 +76,19 @@ new "Check eth/0/0 added using xpath" expecteof "$clixon_netconf -qf $cfg" 0 "$(cat $tmp)" '^eth/0/0ex:ethtrue]]>]]>$' new "Re-create same eth/0/0 which should generate error" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^' new "Delete eth/0/0 using none config" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^]]>]]>$' new "Check deleted eth/0/0 (non-presence container)" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^]]>]]>$' new "Re-Delete eth/0/0 using none should generate error" -expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^' +expecteof "$clixon_netconf -qf $cfg" 0 'eth/0/0ex:ethnone ]]>]]>' '^' new "Add interface without key" -expecteof "$clixon_netconf -qf $cfg" 0 'ex:ethnone ]]>]]>' '^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 4f0a57f3..7dcf82a0 100755 --- a/test/test_order.sh +++ b/test/test_order.sh @@ -317,7 +317,7 @@ new "check ordered-by-user: a,b,c,d,e" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^abcde]]>]]>$' new "move one entry (e) to leaf-list first" -expecteof "$clixon_netconf -qf $cfg" 0 'e]]>]]>' "^]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 'e]]>]]>' "^]]>]]>$" new "check ordered-by-user: e,a,b,c,d" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '^eabcd]]>]]>$' diff --git a/test/test_perf.sh b/test/test_perf.sh index 65c20531..23e71c7c 100755 --- a/test/test_perf.sh +++ b/test/test_perf.sh @@ -186,7 +186,7 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^< new "netconf delete $perfreq small config" { time -p for (( i=0; i<$perfreq; i++ )); do rnd=$(( ( RANDOM % $perfnr ) )) - echo "$rnd]]>]]>" + echo "$rnd]]>]]>" done | $clixon_netconf -qf $cfg > /dev/null; } 2>&1 | awk '/real/ {print $2}' new "netconf discard-changes" diff --git a/test/test_transaction.sh b/test/test_transaction.sh index fefe6122..279dd1e4 100755 --- a/test/test_transaction.sh +++ b/test/test_transaction.sh @@ -318,4 +318,4 @@ fi # kill backend stop_backend -f $cfg -#rm -rf $dir +rm -rf $dir diff --git a/test/test_unique.sh b/test/test_unique.sh index a921ff53..eb6cca5e 100755 --- a/test/test_unique.sh +++ b/test/test_unique.sh @@ -135,14 +135,14 @@ 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 +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 +expecteof "$clixon_netconf -qf $cfg" 0 'nonesmtp25 ]]>]]>' '^]]>]]>$' new "netconf validate ok" diff --git a/test/test_yang.sh b/test/test_yang.sh index d535a0b8..a448db71 100755 --- a/test/test_yang.sh +++ b/test/test_yang.sh @@ -257,7 +257,7 @@ new "netconf check replace" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '111replace121two]]>]]>' new "netconf delete first" -expecteof "$clixon_netconf -qf $cfg" 0 '111]]>]]>' "^]]>]]>$" +expecteof "$clixon_netconf -qf $cfg" 0 '111]]>]]>' "^]]>]]>$" new "netconf check delete" expecteof "$clixon_netconf -qf $cfg" 0 ']]>]]>' '121two]]>]]>'