From e7b60619daf9b164a3f06d1b13c910e060299b81 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Tue, 23 Jul 2019 22:11:14 +0200 Subject: [PATCH] * 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."; + } + } + } + +}