From c1bae276ffbcd1a8b70c27c0be5d3fdd88e520a5 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 25 Jul 2019 13:15:31 +0200 Subject: [PATCH] * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: * `201 Created` for created resources * `204 No Content` for replaced resources. * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * HTTP `Location:` fields added in RESTCONF POST replies --- CHANGELOG.md | 15 +- README.md | 1 + apps/restconf/Makefile.in | 2 + apps/restconf/clixon_restconf.h | 4 +- apps/restconf/restconf_lib.c | 54 +- apps/restconf/restconf_lib.h | 5 +- apps/restconf/restconf_main.c | 8 +- apps/restconf/restconf_methods.c | 1250 ++----------------------- apps/restconf/restconf_methods.h | 27 +- apps/restconf/restconf_methods_get.c | 421 +++++++++ apps/restconf/restconf_methods_get.h | 53 ++ apps/restconf/restconf_methods_post.c | 897 ++++++++++++++++++ apps/restconf/restconf_methods_post.h | 54 ++ apps/restconf/restconf_stream.c | 5 +- apps/restconf/restconf_stream.h | 2 +- lib/clixon/clixon_xml_map.h | 1 + lib/clixon/clixon_xpath.h | 2 +- lib/clixon/clixon_xpath_ctx.h | 2 +- lib/src/clixon_xml_map.c | 173 +++- lib/src/clixon_xml_sort.c | 13 +- lib/src/clixon_xpath.c | 2 +- lib/src/clixon_xpath_ctx.c | 2 +- test/test_nacm_default.sh | 2 +- test/test_nacm_module_write.sh | 4 - test/test_restconf.sh | 2 +- test/test_restconf_jukebox.sh | 25 +- test/test_submodule.sh | 6 +- 27 files changed, 1734 insertions(+), 1298 deletions(-) create mode 100644 apps/restconf/restconf_methods_get.c create mode 100644 apps/restconf/restconf_methods_get.h create mode 100644 apps/restconf/restconf_methods_post.c create mode 100644 apps/restconf/restconf_methods_post.h diff --git a/CHANGELOG.md b/CHANGELOG.md index cc712d2d..941ffcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,23 @@ ### Major New features * Restconf RFC 8040 increased feature compliance - * Cache-Control: no-cache added in HTTP responses (RFC Section 5.5) + * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: + * `201 Created` for created resources + * `204 No Content` for replaced resources. + * See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) + * Implementation detail: due to difference between RESTCONF and NETCONF semantics, a PUT first to make en internal netconf edit-config create operation; if that fails, a replace operation is tried. + * HTTP `Location:` fields added in RESTCONF POST replies + * HTTP `Cache-Control: no-cache` fields added in HTTP responses (RFC Section 5.5) * Restconf monitoring capabilities (RFC Section 9.1) -* Added support for Yang extensions + * +* Yang extensions support * New plugin callback: ca_extension * Main backend example includes example code on how to implement a Yang extension in a plugin. ### API changes on existing features (you may need to change your code) +* RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: + * `201 Created` for created resources + * `204 No Content` for replaced resources. * JSON changes * Non-pretty-print output removed all extra spaces. * Example: `{"nacm-example:x": 42}` --> {"nacm-example:x":42}` @@ -23,6 +33,7 @@ * pseudo-plugin added, to enable callbacks also for main programs. Useful for extensions ### Corrected Bugs +* See [RESTCONF: HTTP return codes are not according to RFC 8040](https://github.com/clicon/clixon/issues/56) * Yang Unique statements with multiple schema identifiers did not work on some platforms due to memory error. ## 4.0.0 (13 July 2019) diff --git a/README.md b/README.md index 91c10429..9fb894e8 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,7 @@ The following features are supported: - query parameters start-time and stop-time(RFC8040 section 4.9) The following features are not implemented: +- ETag/Last-Modified - PATCH - query parameters other than start/stop-time. diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index 9185c60c..2e9629ac 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -76,6 +76,8 @@ APPL = clixon_restconf # Not accessible from plugin APPSRC = restconf_main.c APPSRC += restconf_methods.c +APPSRC += restconf_methods_post.c +APPSRC += restconf_methods_get.c APPSRC += restconf_stream.c APPOBJ = $(APPSRC:.c=.o) diff --git a/apps/restconf/clixon_restconf.h b/apps/restconf/clixon_restconf.h index 06c6ed86..38385d40 100644 --- a/apps/restconf/clixon_restconf.h +++ b/apps/restconf/clixon_restconf.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -51,7 +51,7 @@ int notfound(FCGX_Request *r); int conflict(FCGX_Request *r); int internal_server_error(FCGX_Request *r); int notimplemented(FCGX_Request *r); -int test(FCGX_Request *r, int dbg); +int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index 05cf76e8..e4d507f3 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -31,6 +31,7 @@ ***** END LICENSE BLOCK ***** + * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https */ #include @@ -302,12 +303,12 @@ printparam(FCGX_Request *r, return 0; } -/*! +/*! Print all FCGI headers * @param[in] r Fastcgi request handle */ int -test(FCGX_Request *r, - int dbg) +restconf_test(FCGX_Request *r, + int dbg) { printparam(r, "QUERY_STRING", dbg); printparam(r, "REQUEST_METHOD", dbg); @@ -328,6 +329,7 @@ test(FCGX_Request *r, printparam(r, "SERVER_NAME", dbg); printparam(r, "HTTP_COOKIE", dbg); printparam(r, "HTTPS", dbg); + printparam(r, "HTTP_HOST", dbg); printparam(r, "HTTP_ACCEPT", dbg); printparam(r, "HTTP_CONTENT_TYPE", dbg); printparam(r, "HTTP_AUTHORIZATION", dbg); @@ -478,6 +480,50 @@ api_return_err(clicon_handle h, return retval; } +/*! Print location header from FCGI environment + * @param[in] r Fastcgi request handle + * @param[in] xobj If set (eg POST) add to api-path + * $https “on” if connection operates in SSL mode, or an empty string otherwise + * @note ports are ignored + */ +int +http_location(FCGX_Request *r, + cxobj *xobj) +{ + int retval = -1; + char *https; + char *host; + char *request_uri; + cbuf *cb = NULL; + + https = FCGX_GetParam("HTTPS", r->envp); + host = FCGX_GetParam("HTTP_HOST", r->envp); + request_uri = FCGX_GetParam("REQUEST_URI", r->envp); + if (xobj != NULL){ + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + if (xml2api_path_1(xobj, cb) < 0) + goto done; + FCGX_FPrintF(r->out, "Location: http%s://%s%s%s\r\n", + https?"s":"", + host, + request_uri, + cbuf_get(cb)); + } + else + FCGX_FPrintF(r->out, "Location: http%s://%s%s\r\n", + https?"s":"", + host, + request_uri); + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + /*! Clean and close all state of restconf process (but dont exit). * Cannot use h after this * @param[in] h Clixon handle diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 1b90ef30..7270807e 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -56,11 +56,12 @@ int conflict(FCGX_Request *r); int internal_server_error(FCGX_Request *r); int notimplemented(FCGX_Request *r); -int test(FCGX_Request *r, int dbg); +int restconf_test(FCGX_Request *r, int dbg); cbuf *readdata(FCGX_Request *r); int get_user_cookie(char *cookiestr, char *attribute, char **val); int api_return_err(clicon_handle h, FCGX_Request *r, cxobj *xerr, int pretty, int use_xml, int code); +int http_location(FCGX_Request *r, cxobj *xobj); int restconf_terminate(clicon_handle h); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index e8cbe8a2..3f042975 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -78,6 +78,8 @@ /* restconf */ #include "restconf_lib.h" #include "restconf_methods.h" +#include "restconf_methods_post.h" +#include "restconf_methods_get.h" #include "restconf_stream.h" /* Command line options to be passed to getopt(3) */ @@ -368,7 +370,7 @@ api_restconf(clicon_handle h, retval = notfound(r); goto done; } - test(r, 1); + restconf_test(r, 1); if (pn == 2){ retval = api_root(h, r); @@ -429,7 +431,7 @@ api_restconf(clicon_handle h, goto done; } else if (strcmp(method, "test") == 0) - test(r, 0); + restconf_test(r, 0); else notfound(r); ok: diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 4208eca2..7d84cf92 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -146,549 +146,6 @@ api_data_options(clicon_handle h, return 0; } - -/*! Generic GET (both HEAD and GET) - * According to restconf - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @param[in] head If 1 is HEAD, otherwise GET - * @code - * curl -G http://localhost/restconf/data/interfaces/interface=eth0 - * @endcode - * See RFC8040 Sec 4.2 and 4.3 - * XXX: cant find a way to use Accept request field to choose Content-Type - * I would like to support both xml and json. - * Request may contain - * Accept: application/yang.data+json,application/yang.data+xml - * Response contains one of: - * Content-Type: application/yang-data+xml - * Content-Type: application/yang-data+json - * NOTE: If a retrieval request for a data resource representing a YANG leaf- - * list or list object identifies more than one instance, and XML - * encoding is used in the response, then an error response containing a - * "400 Bad Request" status-line MUST be returned by the server. - * Netconf: , - */ -static int -api_data_get2(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml, - int head) -{ - int retval = -1; - cbuf *cbpath = NULL; - char *xpath = NULL; - cbuf *cbx = NULL; - yang_stmt *yspec; - cxobj *xret = NULL; - cxobj *xerr = NULL; /* malloced */ - cxobj *xe = NULL; /* not malloced */ - cxobj **xvec = NULL; - size_t xlen; - int i; - cxobj *x; - int ret; - char *namespace = NULL; - cvec *nsc = NULL; - - clicon_debug(1, "%s", __FUNCTION__); - yspec = clicon_dbspec_yang(h); - if ((cbpath = cbuf_new()) == NULL) - goto done; - cprintf(cbpath, "/"); - /* We know "data" is element pi-1 */ - if ((ret = api_path2xpath_cvv(pcvec, pi, yspec, cbpath, &namespace)) < 0) - goto done; - if (ret == 0){ - if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) - goto done; - clicon_err_reset(); - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - xpath = cbuf_get(cbpath); - clicon_debug(1, "%s path:%s", __FUNCTION__, xpath); - /* Create a namespace context for ymod as the default namespace to use with - * xpath expressions */ - if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) - goto done; - if (clicon_rpc_get(h, xpath, namespace, &xret) < 0){ - if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - if (xml_apply(xret, CX_ELMNT, xml_spec_populate, yspec) < 0) - goto done; - /* We get return via netconf which is complete tree from root - * We need to cut that tree to only the object. - */ -#if 0 /* DEBUG */ - if (debug){ - cbuf *cb = cbuf_new(); - clicon_xml2cbuf(cb, xret, 0, 0); - clicon_debug(1, "%s xret:%s", __FUNCTION__, cbuf_get(cb)); - cbuf_free(cb); - } -#endif - /* Check if error return */ - if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Normal return, no error */ - if ((cbx = cbuf_new()) == NULL) - goto done; - if (head){ - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - goto ok; - } - if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ - if (use_xml){ - if (clicon_xml2cbuf(cbx, xret, 0, pretty) < 0) /* Dont print top object? */ - goto done; - } - else{ - if (xml2json_cbuf(cbx, xret, pretty) < 0) - goto done; - } - } - else{ - if (xpath_vec_nsc(xret, nsc, "%s", &xvec, &xlen, xpath) < 0){ - if (netconf_operation_failed_xml(&xerr, "application", clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Check if not exists */ - if (xlen == 0){ - /* 4.3: If a retrieval request for a data resource represents an - instance that does not exist, then an error response containing - a "404 Not Found" status-line MUST be returned by the server. - The error-tag value "invalid-value" is used in this case. */ - if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0) - goto done; - /* override invalid-value default 400 with 404 */ - if (api_return_err(h, r, xerr, pretty, use_xml, 404) < 0) - goto done; - goto ok; - } - if (use_xml){ - for (i=0; i0 - * Out: {"example:x": {"0"}} - */ - if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) - goto done; - } - } - clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\r\n\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (nsc) - xml_nsctx_free(nsc); - if (cbx) - cbuf_free(cbx); - if (cbpath) - cbuf_free(cbpath); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (xvec) - free(xvec); - return retval; -} - -/*! REST HEAD method - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * - * The HEAD method is sent by the client to retrieve just the header fields - * that would be returned for the comparable GET method, without the - * response message-body. - * Relation to netconf: none - */ -int -api_data_head(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml) -{ - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 1); -} - -/*! REST GET method - * According to restconf - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * @code - * curl -G http://localhost/restconf/data/interfaces/interface=eth0 - * @endcode - * XXX: cant find a way to use Accept request field to choose Content-Type - * I would like to support both xml and json. - * Request may contain - * Accept: application/yang.data+json,application/yang.data+xml - * Response contains one of: - * Content-Type: application/yang-data+xml - * Content-Type: application/yang-data+json - * NOTE: If a retrieval request for a data resource representing a YANG leaf- - * list or list object identifies more than one instance, and XML - * encoding is used in the response, then an error response containing a - * "400 Bad Request" status-line MUST be returned by the server. - * Netconf: , - */ -int -api_data_get(clicon_handle h, - FCGX_Request *r, - cvec *pcvec, - int pi, - cvec *qvec, - int pretty, - int use_xml) -{ - return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 0); -} - -/*! Generic REST POST method - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where to start pcvec - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - - * @note restconf POST is mapped to edit-config create. - * See RFC8040 Sec 4.4.1 - - POST: - target resource type is datastore --> create a top-level resource - target resource type is data resource --> create child resource - - The message-body MUST contain exactly one instance of the - expected data resource. The data model for the child tree is the - subtree, as defined by YANG for the child resource. - - If the POST method succeeds, a "201 Created" status-line is returned - and there is no response message-body. A "Location" header - identifying the child resource that was created MUST be present in - the response in this case. - - If the data resource already exists, then the POST request MUST fail - and a "409 Conflict" status-line MUST be returned. - * Netconf: (nc:operation="create") | invoke an RPC operation * @example - */ -int -api_data_post(clicon_handle h, - FCGX_Request *r, - char *api_path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - enum operation_type op = OP_CREATE; - cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ - cxobj *xdata; /* -d data (without top symbol)*/ - int i; - cbuf *cbx = NULL; - cxobj *xtop = NULL; /* top of api-path */ - cxobj *xbot = NULL; /* bottom of api-path */ - yang_stmt *ybot = NULL; /* yang of xbot */ - yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ - yang_stmt *ymoddata = NULL; /* yang module of data (-d) */ - yang_stmt *yspec; - cxobj *xa; - cxobj *xret = NULL; - cxobj *xretcom = NULL; /* return from commit */ - cxobj *xretdis = NULL; /* return from discard-changes */ - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xe; /* dont free */ - char *username; - int ret; - - clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", - __FUNCTION__, - api_path, data); - if ((yspec = clicon_dbspec_yang(h)) == NULL){ - clicon_err(OE_FATAL, 0, "No DB_SPEC"); - goto done; - } - for (i=0; i", username?username:""); - cprintf(cbx, ""); - cprintf(cbx, "none"); - if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) - goto done; - cprintf(cbx, ""); - clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) - goto done; - if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Assume this is validation failed since commit includes validate */ - cbuf_reset(cbx); - /* commit/discard should be done automaticaly by the system, therefore - * recovery user is used here (edit-config but not commit may be permitted - by NACM */ - cprintf(cbx, "", NACM_RECOVERY_USER); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) - goto done; - if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ - cbuf_reset(cbx); - cprintf(cbx, "", username?username:""); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretdis, NULL) < 0) - goto done; - /* log errors from discard, but ignore */ - if ((xpath_first(xretdis, "//rpc-error")) != NULL) - clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) /* Use original xe */ - goto done; - goto ok; - } - if (xretcom){ /* Clear: can be reused again below */ - xml_free(xretcom); - xretcom = NULL; - } - if (if_feature(yspec, "ietf-netconf", "startup")){ - /* RFC8040 Sec 1.4: - * If the NETCONF server supports :startup, the RESTCONF server MUST - * automatically update the non-volatile startup configuration - * datastore, after the "running" datastore has been altered as a - * consequence of a RESTCONF edit operation. - */ - cbuf_reset(cbx); - cprintf(cbx, "", NACM_RECOVERY_USER); - cprintf(cbx, ""); - if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) - goto done; - /* If copy-config failed, log and ignore (already committed) */ - if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ - - clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); - } - } - FCGX_SetExitStatus(201, r->out); /* Created */ - FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (xretcom) - xml_free(xretcom); - if (xretdis) - xml_free(xretdis); - if (xtop) - xml_free(xtop); - if (xdata0) - xml_free(xdata0); - if (cbx) - cbuf_free(cbx); - return retval; -} /* api_data_post */ - - /*! Check matching keys * * Check that x1 and x2 are of type list/leaf-list and share the same key statements @@ -771,16 +228,27 @@ match_list_keys(yang_stmt *y, * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data * @note restconf PUT is mapped to edit-config replace. - * See RFC8040 Sec 4.5 + * @see RFC8040 Sec 4.5 PUT * @example curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 * PUT: - if the PUT request creates a new resource, - a "201 Created" status-line is returned. If an existing resource is - modified, a "204 No Content" status-line is returned. + A request message-body MUST be present, representing the new data resource, or the server + MUST return a "400 Bad Request" status-line. + + ...if the PUT request creates a new resource, a "201 Created" status-line is returned. + If an existing resource is modified, a "204 No Content" status-line is returned. * Netconf: (nc:operation="create/replace") + * Note RFC8040 says that if an object is created, 201 is returned, if replaced 204 + * is returned. But the restconf client does not know if it is replaced or created, + * only the server knows that. Solutions: + * 1) extend the netconf so it returns if created/replaced. But that would lead + * to extension of netconf that may hit other places. + * 2) Send a get first and see if the resource exists, and then send replace/create. + * Will always produce an extra message and the GET may potetnially waste bw. + * 3) Try to create first, if that fails (with conflict) then try replace. + * --> Best solution and applied here */ int api_data_put(clicon_handle h, @@ -795,7 +263,7 @@ api_data_put(clicon_handle h, int parse_xml) { int retval = -1; - enum operation_type op = OP_REPLACE; + enum operation_type op; int i; cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ cxobj *xdata; /* -d data (without top symbol)*/ @@ -924,10 +392,12 @@ api_data_put(clicon_handle h, } } - /* Add operation (create/replace) as attribute */ + /* Add operation create as attribute. If that fails with Conflict, then try + "replace" */ if ((xa = xml_new("operation", xdata, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); + op = OP_CREATE; if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; @@ -1032,6 +502,7 @@ api_data_put(clicon_handle h, /* For internal XML protocol: add username attribute for access control */ username = clicon_username_get(h); + again: cprintf(cbx, "", username?username:""); cprintf(cbx, ""); cprintf(cbx, "none"); @@ -1042,9 +513,30 @@ api_data_put(clicon_handle h, if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) goto done; if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + /* If the error is not data-exists, then return error now + * OR we have run again with replace + */ + if (xpath_first(xe, ".[error-tag=\"data-exists\"]") == NULL || + op == OP_REPLACE){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* If it is data-exists, then set operator to replace and try again */ + if (xret){ + xml_free(xret); + xret = NULL; + } + if ((xa = xml_find_type(xdata, NULL, "operation", CX_ATTR)) == NULL){ + clicon_err(OE_XML, ENOENT, "operation attr not found (shouldnt happen)"); goto done; - goto ok; + } + op = OP_REPLACE; + if (xml_value_set(xa, xml_operation2str(op)) < 0) + goto done; + cbuf_reset(cbx); + clicon_debug(1, "%s Failed with create, trying replace",__FUNCTION__); + goto again; } cbuf_reset(cbx); /* commit/discard should be done automaticaly by the system, therefore @@ -1089,7 +581,15 @@ api_data_put(clicon_handle h, clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } - FCGX_SetExitStatus(201, r->out); /* Created */ + /* Check if it was created, or if we tried again and replaced it */ + if (op == OP_CREATE){ + FCGX_SetExitStatus(201, r->out); /* Created */ + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); + } + else{ + FCGX_SetExitStatus(204, r->out); /* Replaced */ + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); + } FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); ok: @@ -1226,7 +726,7 @@ api_data_delete(clicon_handle h, } /* Assume this is validation failed since commit includes validate */ cbuf_reset(cbx); - /* commit/discard should be done automaticaly by the system, therefore + /* commit/discard should be done automatically by the system, therefore * recovery user is used here (edit-config but not commit may be permitted by NACM */ cprintf(cbx, "", NACM_RECOVERY_USER); @@ -1268,7 +768,8 @@ api_data_delete(clicon_handle h, clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); } } - FCGX_SetExitStatus(201, r->out); + FCGX_SetExitStatus(204, r->out); + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); ok: @@ -1288,642 +789,3 @@ api_data_delete(clicon_handle h, return retval; } -/*! GET restconf/operations resource - * @param[in] h Clixon handle - * @param[in] r Fastcgi request handle - * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where path starts - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML - * - * @code - * curl -G http://localhost/restconf/operations - * @endcode - * RFC8040 Sec 3.3.2: - * This optional resource is a container that provides access to the - * data-model-specific RPC operations supported by the server. The - * server MAY omit this resource if no data-model-specific RPC - * operations are advertised. - * From ietf-restconf.yang: - * In XML, the YANG module namespace identifies the module: - * - * In JSON, the YANG module name identifies the module: - * { 'ietf-system:system-restart' : [null] } - */ -int -api_operations_get(clicon_handle h, - FCGX_Request *r, - char *path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml) -{ - int retval = -1; - yang_stmt *yspec; - yang_stmt *ymod; /* yang module */ - yang_stmt *yc; - char *namespace; - cbuf *cbx = NULL; - cxobj *xt = NULL; - int i; - - clicon_debug(1, "%s", __FUNCTION__); - yspec = clicon_dbspec_yang(h); - if ((cbx = cbuf_new()) == NULL) - goto done; - if (use_xml) - cprintf(cbx, ""); - else - cprintf(cbx, "{\"operations\": {"); - ymod = NULL; - i = 0; - while ((ymod = yn_each(yspec, ymod)) != NULL) { - namespace = yang_find_mynamespace(ymod); - yc = NULL; - while ((yc = yn_each(ymod, yc)) != NULL) { - if (yang_keyword_get(yc) != Y_RPC) - continue; - if (use_xml) - cprintf(cbx, "<%s xmlns=\"%s\"/>", yang_argument_get(yc), namespace); - else{ - if (i++) - cprintf(cbx, ","); - cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); - } - } - } - if (use_xml) - cprintf(cbx, ""); - else - cprintf(cbx, "}}"); - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); - FCGX_FPrintF(r->out, "\r\n\r\n"); - // ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (cbx) - cbuf_free(cbx); - if (xt) - xml_free(xt); - return retval; -} - -/*! Handle input data to api_operations_post - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] data Stream input data - * @param[in] yspec Yang top-level specification - * @param[in] yrpc Yang rpc spec - * @param[in] xrpc XML pointer to rpc method - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - * @retval 1 OK - * @retval 0 Fail, Error message sent - * @retval -1 Fatal error, clicon_err called - * - * RFC8040 3.6.1 - * If the "rpc" or "action" statement has an "input" section, then - * instances of these input parameters are encoded in the module - * namespace where the "rpc" or "action" statement is defined, in an XML - * element or JSON object named "input", which is in the module - * namespace where the "rpc" or "action" statement is defined. - * (Any other input is assumed as error.) - */ -static int -api_operations_post_input(clicon_handle h, - FCGX_Request *r, - char *data, - yang_stmt *yspec, - yang_stmt *yrpc, - cxobj *xrpc, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - cxobj *xdata = NULL; - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xe; - cxobj *xinput; - cxobj *x; - cbuf *cbret = NULL; - int ret; - - clicon_debug(1, "%s %s", __FUNCTION__, data); - if ((cbret = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, 0, "cbuf_new"); - goto done; - } - /* Parse input data as json or xml into xml */ - if (parse_xml){ - if (xml_parse_string(data, yspec, &xdata) < 0){ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - } - else { /* JSON */ - if ((ret = json_parse_str(data, yspec, &xdata, &xerr)) < 0){ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - if (ret == 0){ - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - } - xml_name_set(xdata, "data"); - /* Here xdata is: - * ... - */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) - goto done; - clicon_debug(1, "%s DATA:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* Validate that exactly only tag */ - if ((xinput = xml_child_i_type(xdata, 0, CX_ELMNT)) == NULL || - strcmp(xml_name(xinput),"input") != 0 || - xml_child_nr_type(xdata, CX_ELMNT) != 1){ - - if (xml_child_nr_type(xdata, CX_ELMNT) == 0){ - if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have input statement") < 0) - goto done; - } - else - if (netconf_malformed_message_xml(&xerr, "restconf RPC has malformed input statement (multiple or not called input)") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - // clicon_debug(1, "%s input validation passed", __FUNCTION__); - /* Add all input under path */ - x = NULL; - while ((x = xml_child_i_type(xinput, 0, CX_ELMNT)) != NULL) - if (xml_addsub(xrpc, x) < 0) - goto done; - /* Here xrpc is: 42 - */ - // ok: - retval = 1; - done: - clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); - if (cbret) - cbuf_free(cbret); - if (xerr) - xml_free(xerr); - if (xdata) - xml_free(xdata); - return retval; - fail: - retval = 0; - goto done; -} - -/*! Handle output data to api_operations_post - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] xret XML reply messages from backend/handler - * @param[in] yspec Yang top-level specification - * @param[in] youtput Yang rpc output specification - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[out] xoutputp Restconf JSON/XML output - * @retval 1 OK - * @retval 0 Fail, Error message sent - * @retval -1 Fatal error, clicon_err called - * xret should like: 0 - */ -static int -api_operations_post_output(clicon_handle h, - FCGX_Request *r, - cxobj *xret, - yang_stmt *yspec, - yang_stmt *youtput, - char *namespace, - int pretty, - int use_xml, - cxobj **xoutputp) - -{ - int retval = -1; - cxobj *xoutput = NULL; - cxobj *xerr = NULL; /* assumed malloced, will be freed */ - cxobj *xe; /* just pointer */ - cxobj *xa; /* xml attribute (xmlns) */ - cxobj *x; - cxobj *xok; - int isempty; - - // clicon_debug(1, "%s", __FUNCTION__); - /* Validate that exactly only tag */ - if ((xoutput = xml_child_i_type(xret, 0, CX_ELMNT)) == NULL || - strcmp(xml_name(xoutput),"rpc-reply") != 0 || - xml_child_nr_type(xret, CX_ELMNT) != 1){ - if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have single input") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } - /* xoutput should now look: 0 */ - /* 9. Translate to restconf RPC data */ - xml_name_set(xoutput, "output"); - /* xoutput should now look: 0 */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xoutput, 0, 0) < 0) - goto done; - clicon_debug(1, "%s XOUTPUT:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - - /* Sanity check of outgoing XML - * For now, skip outgoing checks. - * (1) Does not handle properly - * (2) Uncertain how validation errors should be logged/handled - */ - if (youtput!=NULL){ - xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ -#if 0 - if (xml_apply(xoutput, CX_ELMNT, xml_spec_populate, yspec) < 0) - goto done; - if ((ret = xml_yang_validate_all(xoutput, &xerr)) < 0) - goto done; - if (ret == 1 && - (ret = xml_yang_validate_add(h, xoutput, &xerr)) < 0) - goto done; - if (ret == 0){ /* validation failed */ - if ((xe = xpath_first(xerr, "rpc-reply/rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto fail; - } -#endif - } - /* Special case, no yang output (single - or empty body?) - * RFC 7950 7.14.4 - * If the RPC operation invocation succeeded and no output parameters - * are returned, the contains a single element - * RFC 8040 3.6.2 - * If the "rpc" statement has no "output" section, the response message - * MUST NOT include a message-body and MUST send a "204 No Content" - * status-line instead. - */ - isempty = xml_child_nr_type(xoutput, CX_ELMNT) == 0 || - (xml_child_nr_type(xoutput, CX_ELMNT) == 1 && - (xok = xml_child_i_type(xoutput, 0, CX_ELMNT)) != NULL && - strcmp(xml_name(xok),"ok")==0); - if (isempty) { - /* Internal error - invalid output from rpc handler */ - FCGX_SetExitStatus(204, r->out); /* OK */ - FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); - FCGX_FPrintF(r->out, "\r\n"); - goto fail; - } - /* Clear namespace of parameters */ - x = NULL; - while ((x = xml_child_each(xoutput, x, CX_ELMNT)) != NULL) { - if ((xa = xml_find_type(x, NULL, "xmlns", CX_ATTR)) != NULL) - if (xml_purge(xa) < 0) - goto done; - } - /* Set namespace on output */ - if (xmlns_set(xoutput, NULL, namespace) < 0) - goto done; - *xoutputp = xoutput; - retval = 1; - done: - clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); - if (xerr) - xml_free(xerr); - return retval; - fail: - retval = 0; - goto done; -} - -/*! REST operation POST method - * @param[in] h CLIXON handle - * @param[in] r Fastcgi request handle - * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) - * @param[in] pcvec Vector of path ie DOCUMENT_URI element - * @param[in] pi Offset, where to start pcvec - * @param[in] qvec Vector of query string (QUERY_STRING) - * @param[in] data Stream input data - * @param[in] pretty Set to 1 for pretty-printed xml/json output - * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data - * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data - * See RFC 8040 Sec 3.6 / 4.4.2 - * @note We map post to edit-config create. - * POST {+restconf}/operations/ - * 1. Initialize - * 2. Get rpc module and name from uri (oppath) and find yang spec - * 3. Build xml tree with user and rpc: - * 4. Parse input data (arguments): - * JSON: {"example:input":{"x":0}} - * XML: 0 - * 5. Translate input args to Netconf RPC, add to xml tree: - * 42 - * 6. Validate outgoing RPC and fill in default values - * 4299 - * 7. Send to RPC handler, either local or backend - * 8. Receive reply from local/backend handler as Netconf RPC - * 0 - * 9. Translate to restconf RPC data: - * JSON: {"example:output":{"x":0}} - * XML: 0 - * 10. Validate and send reply to originator - */ -int -api_operations_post(clicon_handle h, - FCGX_Request *r, - char *path, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - int use_xml, - int parse_xml) -{ - int retval = -1; - int i; - char *oppath = path; - yang_stmt *yspec; - yang_stmt *youtput = NULL; - yang_stmt *yrpc = NULL; - cxobj *xret = NULL; - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xtop = NULL; /* xpath root */ - cxobj *xbot = NULL; - yang_stmt *y = NULL; - cxobj *xoutput = NULL; - cxobj *xa; - cxobj *xe; - char *username; - cbuf *cbret = NULL; - int ret = 0; - char *prefix = NULL; - char *id = NULL; - yang_stmt *ys = NULL; - char *namespace = NULL; - - clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); - /* 1. Initialize */ - if ((yspec = clicon_dbspec_yang(h)) == NULL){ - clicon_err(OE_FATAL, 0, "No DB_SPEC"); - goto done; - } - if ((cbret = cbuf_new()) == NULL){ - clicon_err(OE_UNIX, 0, "cbuf_new"); - goto done; - } - for (i=0; i - * - * The field identifies the module name and rpc identifier - * string for the desired operation. - */ - if (nodeid_split(oppath+1, &prefix, &id) < 0) /* +1 skip / */ - goto done; - if ((ys = yang_find(yspec, Y_MODULE, prefix)) == NULL){ - if (netconf_operation_failed_xml(&xerr, "protocol", "yang module not found") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - if ((yrpc = yang_find(ys, Y_RPC, id)) == NULL){ - if (netconf_missing_element_xml(&xerr, "application", id, "RPC not defined") < 0) - goto done; - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* 3. Build xml tree with user and rpc: - * - */ - if ((xtop = xml_new("rpc", NULL, NULL)) == NULL) - goto done; - xbot = xtop; - /* Here xtop is: */ - if ((username = clicon_username_get(h)) != NULL){ - if ((xa = xml_new("username", xtop, NULL)) == NULL) - goto done; - xml_type_set(xa, CX_ATTR); - if (xml_value_set(xa, username) < 0) - goto done; - /* Here xtop is: */ - } - if ((ret = api_path2xml(oppath, yspec, xtop, YC_SCHEMANODE, 1, &xbot, &y)) < 0) - goto done; - if (ret == 0){ /* validation failed */ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) - goto done; - clicon_err_reset(); - if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto done; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Here xtop is: - * xbot is - * 4. Parse input data (arguments): - * JSON: {"example:input":{"x":0}} - * XML: 0 - */ - namespace = xml_find_type_value(xbot, NULL, "xmlns", CX_ATTR); - clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); - if (data && strlen(data)){ - if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, - pretty, use_xml, parse_xml)) < 0) - goto done; - if (ret == 0) - goto ok; - } - /* Here xtop is: - 42 */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 5. Translate input args: %s", - __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* 6. Validate incoming RPC and fill in defaults */ - if (xml_spec_populate_rpc(h, xtop, yspec) < 0) /* */ - goto done; - if ((ret = xml_yang_validate_rpc(h, xtop, &xret)) < 0) - goto done; - if (ret == 0){ - if ((xe = xpath_first(xret, "rpc-error")) == NULL){ - clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); - goto ok; - } - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - /* Here xtop is (default values): - * 4299 - */ -#if 0 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 6. Validate and defaults:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - /* 7. Send to RPC handler, either local or backend - * Note (1) xtop is xbot is - * (2) local handler wants and backend wants - */ - /* Look for local (client-side) restconf plugins. - * -1:Error, 0:OK local, 1:OK backend - */ - if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) - goto done; - if (ret > 0){ /* Handled locally */ - if (xml_parse_string(cbuf_get(cbret), NULL, &xret) < 0) - goto done; - /* Local error: return it and quit */ - if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - } - else { /* Send to backend */ - if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) - goto done; - if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ - if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) - goto done; - goto ok; - } - } - /* 8. Receive reply from local/backend handler as Netconf RPC - * 0 - */ -#if 1 - if (debug){ - cbuf *ccc=cbuf_new(); - if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) - goto done; - clicon_debug(1, "%s 8. Receive reply:%s", __FUNCTION__, cbuf_get(ccc)); - } -#endif - youtput = yang_find(yrpc, Y_OUTPUT, NULL); - if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, - pretty, use_xml, &xoutput)) < 0) - goto done; - if (ret == 0) - goto ok; - /* xoutput should now look: 0 */ - FCGX_SetExitStatus(200, r->out); /* OK */ - FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); - FCGX_FPrintF(r->out, "\r\n"); - cbuf_reset(cbret); - if (use_xml){ - if (clicon_xml2cbuf(cbret, xoutput, 0, pretty) < 0) - goto done; - /* xoutput should now look: 0 */ - } - else{ - if (xml2json_cbuf(cbret, xoutput, pretty) < 0) - goto done; - /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ - } - FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); - FCGX_FPrintF(r->out, "\r\n\r\n"); - ok: - retval = 0; - done: - clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); - if (prefix) - free(prefix); - if (id) - free(id); - if (xtop) - xml_free(xtop); - if (xret) - xml_free(xret); - if (xerr) - xml_free(xerr); - if (cbret) - cbuf_free(cbret); - return retval; -} diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 42525946..89ad6460 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -30,29 +30,18 @@ the terms of any one of the Apache License version 2 or the GPL. ***** END LICENSE BLOCK ***** - + + * Restconf method implementation */ #ifndef _RESTCONF_METHODS_H_ #define _RESTCONF_METHODS_H_ -/* - * Constants - */ - /* * Prototypes */ int api_data_options(clicon_handle h, FCGX_Request *r); -int api_data_head(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); -int api_data_get(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, - cvec *qvec, int pretty, int use_xml); -int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, - cvec *pcvec, int pi, - cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, @@ -63,14 +52,4 @@ int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path, int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi, int pretty, int use_xml); -int api_operations_get(clicon_handle h, FCGX_Request *r, - char *path, - cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml); - -int api_operations_post(clicon_handle h, FCGX_Request *r, - char *path, - cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, int use_xml, int parse_xml); - #endif /* _RESTCONF_METHODS_H_ */ diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c new file mode 100644 index 00000000..c89b9ede --- /dev/null +++ b/apps/restconf/restconf_methods_get.c @@ -0,0 +1,421 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for operations get and data get and head + */ + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml-h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_methods.h" +#include "restconf_methods_get.h" + +/*! Generic GET (both HEAD and GET) + * According to restconf + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * @param[in] head If 1 is HEAD, otherwise GET + * @code + * curl -G http://localhost/restconf/data/interfaces/interface=eth0 + * @endcode + * See RFC8040 Sec 4.2 and 4.3 + * XXX: cant find a way to use Accept request field to choose Content-Type + * I would like to support both xml and json. + * Request may contain + * Accept: application/yang.data+json,application/yang.data+xml + * Response contains one of: + * Content-Type: application/yang-data+xml + * Content-Type: application/yang-data+json + * NOTE: If a retrieval request for a data resource representing a YANG leaf- + * list or list object identifies more than one instance, and XML + * encoding is used in the response, then an error response containing a + * "400 Bad Request" status-line MUST be returned by the server. + * Netconf: , + */ +static int +api_data_get2(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml, + int head) +{ + int retval = -1; + cbuf *cbpath = NULL; + char *xpath = NULL; + cbuf *cbx = NULL; + yang_stmt *yspec; + cxobj *xret = NULL; + cxobj *xerr = NULL; /* malloced */ + cxobj *xe = NULL; /* not malloced */ + cxobj **xvec = NULL; + size_t xlen; + int i; + cxobj *x; + int ret; + char *namespace = NULL; + cvec *nsc = NULL; + + clicon_debug(1, "%s", __FUNCTION__); + yspec = clicon_dbspec_yang(h); + if ((cbpath = cbuf_new()) == NULL) + goto done; + cprintf(cbpath, "/"); + /* We know "data" is element pi-1 */ + if ((ret = api_path2xpath_cvv(pcvec, pi, yspec, cbpath, &namespace)) < 0) + goto done; + if (ret == 0){ + if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) + goto done; + clicon_err_reset(); + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + xpath = cbuf_get(cbpath); + clicon_debug(1, "%s path:%s", __FUNCTION__, xpath); + /* Create a namespace context for ymod as the default namespace to use with + * xpath expressions */ + if ((nsc = xml_nsctx_init(NULL, namespace)) == NULL) + goto done; + if (clicon_rpc_get(h, xpath, namespace, &xret) < 0){ + if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + if (xml_apply(xret, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + /* We get return via netconf which is complete tree from root + * We need to cut that tree to only the object. + */ +#if 0 /* DEBUG */ + if (debug){ + cbuf *cb = cbuf_new(); + clicon_xml2cbuf(cb, xret, 0, 0); + clicon_debug(1, "%s xret:%s", __FUNCTION__, cbuf_get(cb)); + cbuf_free(cb); + } +#endif + /* Check if error return */ + if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Normal return, no error */ + if ((cbx = cbuf_new()) == NULL) + goto done; + if (head){ + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + goto ok; + } + if (xpath==NULL || strcmp(xpath,"/")==0){ /* Special case: data root */ + if (use_xml){ + if (clicon_xml2cbuf(cbx, xret, 0, pretty) < 0) /* Dont print top object? */ + goto done; + } + else{ + if (xml2json_cbuf(cbx, xret, pretty) < 0) + goto done; + } + } + else{ + if (xpath_vec_nsc(xret, nsc, "%s", &xvec, &xlen, xpath) < 0){ + if (netconf_operation_failed_xml(&xerr, "application", clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Check if not exists */ + if (xlen == 0){ + /* 4.3: If a retrieval request for a data resource represents an + instance that does not exist, then an error response containing + a "404 Not Found" status-line MUST be returned by the server. + The error-tag value "invalid-value" is used in this case. */ + if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0) + goto done; + /* override invalid-value default 400 with 404 */ + if (api_return_err(h, r, xerr, pretty, use_xml, 404) < 0) + goto done; + goto ok; + } + if (use_xml){ + for (i=0; i0 + * Out: {"example:x": {"0"}} + */ + if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) + goto done; + } + } + clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); + FCGX_FPrintF(r->out, "\r\n\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (nsc) + xml_nsctx_free(nsc); + if (cbx) + cbuf_free(cbx); + if (cbpath) + cbuf_free(cbpath); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (xvec) + free(xvec); + return retval; +} + +/*! REST HEAD method + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * + * The HEAD method is sent by the client to retrieve just the header fields + * that would be returned for the comparable GET method, without the + * response message-body. + * Relation to netconf: none + */ +int +api_data_head(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml) +{ + return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 1); +} + +/*! REST GET method + * According to restconf + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * @code + * curl -G http://localhost/restconf/data/interfaces/interface=eth0 + * @endcode + * XXX: cant find a way to use Accept request field to choose Content-Type + * I would like to support both xml and json. + * Request may contain + * Accept: application/yang.data+json,application/yang.data+xml + * Response contains one of: + * Content-Type: application/yang-data+xml + * Content-Type: application/yang-data+json + * NOTE: If a retrieval request for a data resource representing a YANG leaf- + * list or list object identifies more than one instance, and XML + * encoding is used in the response, then an error response containing a + * "400 Bad Request" status-line MUST be returned by the server. + * Netconf: , + */ +int +api_data_get(clicon_handle h, + FCGX_Request *r, + cvec *pcvec, + int pi, + cvec *qvec, + int pretty, + int use_xml) +{ + return api_data_get2(h, r, pcvec, pi, qvec, pretty, use_xml, 0); +} + +/*! GET restconf/operations resource + * @param[in] h Clixon handle + * @param[in] r Fastcgi request handle + * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where path starts + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML + * + * @code + * curl -G http://localhost/restconf/operations + * @endcode + * RFC8040 Sec 3.3.2: + * This optional resource is a container that provides access to the + * data-model-specific RPC operations supported by the server. The + * server MAY omit this resource if no data-model-specific RPC + * operations are advertised. + * From ietf-restconf.yang: + * In XML, the YANG module namespace identifies the module: + * + * In JSON, the YANG module name identifies the module: + * { 'ietf-system:system-restart' : [null] } + */ +int +api_operations_get(clicon_handle h, + FCGX_Request *r, + char *path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml) +{ + int retval = -1; + yang_stmt *yspec; + yang_stmt *ymod; /* yang module */ + yang_stmt *yc; + char *namespace; + cbuf *cbx = NULL; + cxobj *xt = NULL; + int i; + + clicon_debug(1, "%s", __FUNCTION__); + yspec = clicon_dbspec_yang(h); + if ((cbx = cbuf_new()) == NULL) + goto done; + if (use_xml) + cprintf(cbx, ""); + else + cprintf(cbx, "{\"operations\": {"); + ymod = NULL; + i = 0; + while ((ymod = yn_each(yspec, ymod)) != NULL) { + namespace = yang_find_mynamespace(ymod); + yc = NULL; + while ((yc = yn_each(ymod, yc)) != NULL) { + if (yang_keyword_get(yc) != Y_RPC) + continue; + if (use_xml) + cprintf(cbx, "<%s xmlns=\"%s\"/>", yang_argument_get(yc), namespace); + else{ + if (i++) + cprintf(cbx, ","); + cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); + } + } + } + if (use_xml) + cprintf(cbx, ""); + else + cprintf(cbx, "}}"); + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); + FCGX_FPrintF(r->out, "\r\n\r\n"); + // ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (cbx) + cbuf_free(cbx); + if (xt) + xml_free(xt); + return retval; +} + diff --git a/apps/restconf/restconf_methods_get.h b/apps/restconf/restconf_methods_get.h new file mode 100644 index 00000000..5354d581 --- /dev/null +++ b/apps/restconf/restconf_methods_get.h @@ -0,0 +1,53 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for operations get and data get and head + */ + + +#ifndef _RESTCONF_METHODS_GET_H_ +#define _RESTCONF_METHODS_GET_H_ + +/* + * Prototypes + */ +int api_data_head(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, + cvec *qvec, int pretty, int use_xml); +int api_data_get(clicon_handle h, FCGX_Request *r, cvec *pcvec, int pi, + cvec *qvec, int pretty, int use_xml); +int api_operations_get(clicon_handle h, FCGX_Request *r, + char *path, + cvec *pcvec, int pi, cvec *qvec, char *data, + int pretty, int use_xml); + +#endif /* _RESTCONF_METHODS_GET_H_ */ diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c new file mode 100644 index 00000000..104b37ce --- /dev/null +++ b/apps/restconf/restconf_methods_post.c @@ -0,0 +1,897 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Restconf method implementation for post: operation(rpc) and data + * From RFC 8040 Section 4.4. POST + */ + + +#ifdef HAVE_CONFIG_H +#include "clixon_config.h" /* generated by config & autoconf */ +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* cligen */ +#include + +/* clicon */ +#include + +#include /* Need to be after clixon_xml.h due to attribute format */ + +#include "restconf_lib.h" +#include "restconf_methods.h" +#include "restconf_methods_post.h" + +/*! Generic REST POST method + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + + * restconf POST is mapped to edit-config create. + * @see RFC8040 Sec 4.4.1 + + POST: + target resource type is datastore --> create a top-level resource + target resource type is data resource --> create child resource + + The message-body MUST contain exactly one instance of the + expected data resource. The data model for the child tree is the + subtree, as defined by YANG for the child resource. + + If the POST method succeeds, a "201 Created" status-line is returned + and there is no response message-body. A "Location" header + identifying the child resource that was created MUST be present in + the response in this case. + + If the data resource already exists, then the POST request MUST fail + and a "409 Conflict" status-line MUST be returned. + + * @see RFC8040 Section 4.4 + */ +int +api_data_post(clicon_handle h, + FCGX_Request *r, + char *api_path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + enum operation_type op = OP_CREATE; + cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ + cxobj *xdata; /* -d data (without top symbol)*/ + int i; + cbuf *cbx = NULL; + cxobj *xtop = NULL; /* top of api-path */ + cxobj *xbot = NULL; /* bottom of api-path */ + yang_stmt *ybot = NULL; /* yang of xbot */ + yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ + yang_stmt *ymoddata = NULL; /* yang module of data (-d) */ + yang_stmt *yspec; + cxobj *xa; + cxobj *xret = NULL; + cxobj *xretcom = NULL; /* return from commit */ + cxobj *xretdis = NULL; /* return from discard-changes */ + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; /* dont free */ + char *username; + int ret; + + clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", + __FUNCTION__, + api_path, data); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + for (i=0; i", username?username:""); + cprintf(cbx, ""); + cprintf(cbx, "none"); + if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) + goto done; + cprintf(cbx, ""); + clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 0) + goto done; + if ((xe = xpath_first(xret, "//rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Assume this is validation failed since commit includes validate */ + cbuf_reset(cbx); + /* commit/discard should be done automaticaly by the system, therefore + * recovery user is used here (edit-config but not commit may be permitted + by NACM */ + cprintf(cbx, "", NACM_RECOVERY_USER); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) + goto done; + if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ + cbuf_reset(cbx); + cprintf(cbx, "", username?username:""); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretdis, NULL) < 0) + goto done; + /* log errors from discard, but ignore */ + if ((xpath_first(xretdis, "//rpc-error")) != NULL) + clicon_log(LOG_WARNING, "%s: discard-changes failed which may lead candidate in an inconsistent state", __FUNCTION__); + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) /* Use original xe */ + goto done; + goto ok; + } + if (xretcom){ /* Clear: can be reused again below */ + xml_free(xretcom); + xretcom = NULL; + } + if (if_feature(yspec, "ietf-netconf", "startup")){ + /* RFC8040 Sec 1.4: + * If the NETCONF server supports :startup, the RESTCONF server MUST + * automatically update the non-volatile startup configuration + * datastore, after the "running" datastore has been altered as a + * consequence of a RESTCONF edit operation. + */ + cbuf_reset(cbx); + cprintf(cbx, "", NACM_RECOVERY_USER); + cprintf(cbx, ""); + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xretcom, NULL) < 0) + goto done; + /* If copy-config failed, log and ignore (already committed) */ + if ((xe = xpath_first(xretcom, "//rpc-error")) != NULL){ + + clicon_log(LOG_WARNING, "%s: copy-config running->startup failed", __FUNCTION__); + } + } + FCGX_SetExitStatus(201, r->out); + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); + http_location(r, xdata); + FCGX_GetParam("HTTP_ACCEPT", r->envp); + FCGX_FPrintF(r->out, "\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (xretcom) + xml_free(xretcom); + if (xretdis) + xml_free(xretdis); + if (xtop) + xml_free(xtop); + if (xdata0) + xml_free(xdata0); + if (cbx) + cbuf_free(cbx); + return retval; +} /* api_data_post */ + +/*! Handle input data to api_operations_post + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] data Stream input data + * @param[in] yspec Yang top-level specification + * @param[in] yrpc Yang rpc spec + * @param[in] xrpc XML pointer to rpc method + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * @retval 1 OK + * @retval 0 Fail, Error message sent + * @retval -1 Fatal error, clicon_err called + * + * RFC8040 3.6.1 + * If the "rpc" or "action" statement has an "input" section, then + * instances of these input parameters are encoded in the module + * namespace where the "rpc" or "action" statement is defined, in an XML + * element or JSON object named "input", which is in the module + * namespace where the "rpc" or "action" statement is defined. + * (Any other input is assumed as error.) + */ +static int +api_operations_post_input(clicon_handle h, + FCGX_Request *r, + char *data, + yang_stmt *yspec, + yang_stmt *yrpc, + cxobj *xrpc, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + cxobj *xdata = NULL; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; + cxobj *xinput; + cxobj *x; + cbuf *cbret = NULL; + int ret; + + clicon_debug(1, "%s %s", __FUNCTION__, data); + if ((cbret = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + /* Parse input data as json or xml into xml */ + if (parse_xml){ + if (xml_parse_string(data, yspec, &xdata) < 0){ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + } + else { /* JSON */ + if ((ret = json_parse_str(data, yspec, &xdata, &xerr)) < 0){ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + if (ret == 0){ + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + } + xml_name_set(xdata, "data"); + /* Here xdata is: + * ... + */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xdata, 0, 0) < 0) + goto done; + clicon_debug(1, "%s DATA:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* Validate that exactly only tag */ + if ((xinput = xml_child_i_type(xdata, 0, CX_ELMNT)) == NULL || + strcmp(xml_name(xinput),"input") != 0 || + xml_child_nr_type(xdata, CX_ELMNT) != 1){ + + if (xml_child_nr_type(xdata, CX_ELMNT) == 0){ + if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have input statement") < 0) + goto done; + } + else + if (netconf_malformed_message_xml(&xerr, "restconf RPC has malformed input statement (multiple or not called input)") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + // clicon_debug(1, "%s input validation passed", __FUNCTION__); + /* Add all input under path */ + x = NULL; + while ((x = xml_child_i_type(xinput, 0, CX_ELMNT)) != NULL) + if (xml_addsub(xrpc, x) < 0) + goto done; + /* Here xrpc is: 42 + */ + // ok: + retval = 1; + done: + clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); + if (cbret) + cbuf_free(cbret); + if (xerr) + xml_free(xerr); + if (xdata) + xml_free(xdata); + return retval; + fail: + retval = 0; + goto done; +} + +/*! Handle output data to api_operations_post + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] xret XML reply messages from backend/handler + * @param[in] yspec Yang top-level specification + * @param[in] youtput Yang rpc output specification + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[out] xoutputp Restconf JSON/XML output + * @retval 1 OK + * @retval 0 Fail, Error message sent + * @retval -1 Fatal error, clicon_err called + * xret should like: 0 + */ +static int +api_operations_post_output(clicon_handle h, + FCGX_Request *r, + cxobj *xret, + yang_stmt *yspec, + yang_stmt *youtput, + char *namespace, + int pretty, + int use_xml, + cxobj **xoutputp) + +{ + int retval = -1; + cxobj *xoutput = NULL; + cxobj *xerr = NULL; /* assumed malloced, will be freed */ + cxobj *xe; /* just pointer */ + cxobj *xa; /* xml attribute (xmlns) */ + cxobj *x; + cxobj *xok; + int isempty; + + // clicon_debug(1, "%s", __FUNCTION__); + /* Validate that exactly only tag */ + if ((xoutput = xml_child_i_type(xret, 0, CX_ELMNT)) == NULL || + strcmp(xml_name(xoutput),"rpc-reply") != 0 || + xml_child_nr_type(xret, CX_ELMNT) != 1){ + if (netconf_malformed_message_xml(&xerr, "restconf RPC does not have single input") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } + /* xoutput should now look: 0 */ + /* 9. Translate to restconf RPC data */ + xml_name_set(xoutput, "output"); + /* xoutput should now look: 0 */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xoutput, 0, 0) < 0) + goto done; + clicon_debug(1, "%s XOUTPUT:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + + /* Sanity check of outgoing XML + * For now, skip outgoing checks. + * (1) Does not handle properly + * (2) Uncertain how validation errors should be logged/handled + */ + if (youtput!=NULL){ + xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ +#if 0 + if (xml_apply(xoutput, CX_ELMNT, xml_spec_populate, yspec) < 0) + goto done; + if ((ret = xml_yang_validate_all(xoutput, &xerr)) < 0) + goto done; + if (ret == 1 && + (ret = xml_yang_validate_add(h, xoutput, &xerr)) < 0) + goto done; + if (ret == 0){ /* validation failed */ + if ((xe = xpath_first(xerr, "rpc-reply/rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto fail; + } +#endif + } + /* Special case, no yang output (single - or empty body?) + * RFC 7950 7.14.4 + * If the RPC operation invocation succeeded and no output parameters + * are returned, the contains a single element + * RFC 8040 3.6.2 + * If the "rpc" statement has no "output" section, the response message + * MUST NOT include a message-body and MUST send a "204 No Content" + * status-line instead. + */ + isempty = xml_child_nr_type(xoutput, CX_ELMNT) == 0 || + (xml_child_nr_type(xoutput, CX_ELMNT) == 1 && + (xok = xml_child_i_type(xoutput, 0, CX_ELMNT)) != NULL && + strcmp(xml_name(xok),"ok")==0); + if (isempty) { + /* Internal error - invalid output from rpc handler */ + FCGX_SetExitStatus(204, r->out); /* OK */ + FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); + FCGX_FPrintF(r->out, "\r\n"); + goto fail; + } + /* Clear namespace of parameters */ + x = NULL; + while ((x = xml_child_each(xoutput, x, CX_ELMNT)) != NULL) { + if ((xa = xml_find_type(x, NULL, "xmlns", CX_ATTR)) != NULL) + if (xml_purge(xa) < 0) + goto done; + } + /* Set namespace on output */ + if (xmlns_set(xoutput, NULL, namespace) < 0) + goto done; + *xoutputp = xoutput; + retval = 1; + done: + clicon_debug(1, "%s retval: %d", __FUNCTION__, retval); + if (xerr) + xml_free(xerr); + return retval; + fail: + retval = 0; + goto done; +} + +/*! REST operation POST method + * @param[in] h CLIXON handle + * @param[in] r Fastcgi request handle + * @param[in] path According to restconf (Sec 3.5.1.1 in [draft]) + * @param[in] pcvec Vector of path ie DOCUMENT_URI element + * @param[in] pi Offset, where to start pcvec + * @param[in] qvec Vector of query string (QUERY_STRING) + * @param[in] data Stream input data + * @param[in] pretty Set to 1 for pretty-printed xml/json output + * @param[in] use_xml Set to 0 for JSON and 1 for XML for output data + * @param[in] parse_xml Set to 0 for JSON and 1 for XML for input data + * See RFC 8040 Sec 3.6 / 4.4.2 + * @note We map post to edit-config create. + * POST {+restconf}/operations/ + * 1. Initialize + * 2. Get rpc module and name from uri (oppath) and find yang spec + * 3. Build xml tree with user and rpc: + * 4. Parse input data (arguments): + * JSON: {"example:input":{"x":0}} + * XML: 0 + * 5. Translate input args to Netconf RPC, add to xml tree: + * 42 + * 6. Validate outgoing RPC and fill in default values + * 4299 + * 7. Send to RPC handler, either local or backend + * 8. Receive reply from local/backend handler as Netconf RPC + * 0 + * 9. Translate to restconf RPC data: + * JSON: {"example:output":{"x":0}} + * XML: 0 + * 10. Validate and send reply to originator + */ +int +api_operations_post(clicon_handle h, + FCGX_Request *r, + char *path, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + int use_xml, + int parse_xml) +{ + int retval = -1; + int i; + char *oppath = path; + yang_stmt *yspec; + yang_stmt *youtput = NULL; + yang_stmt *yrpc = NULL; + cxobj *xret = NULL; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xtop = NULL; /* xpath root */ + cxobj *xbot = NULL; + yang_stmt *y = NULL; + cxobj *xoutput = NULL; + cxobj *xa; + cxobj *xe; + char *username; + cbuf *cbret = NULL; + int ret = 0; + char *prefix = NULL; + char *id = NULL; + yang_stmt *ys = NULL; + char *namespace = NULL; + + clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); + /* 1. Initialize */ + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } + if ((cbret = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, 0, "cbuf_new"); + goto done; + } + for (i=0; i + * + * The field identifies the module name and rpc identifier + * string for the desired operation. + */ + if (nodeid_split(oppath+1, &prefix, &id) < 0) /* +1 skip / */ + goto done; + if ((ys = yang_find(yspec, Y_MODULE, prefix)) == NULL){ + if (netconf_operation_failed_xml(&xerr, "protocol", "yang module not found") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + if ((yrpc = yang_find(ys, Y_RPC, id)) == NULL){ + if (netconf_missing_element_xml(&xerr, "application", id, "RPC not defined") < 0) + goto done; + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* 3. Build xml tree with user and rpc: + * + */ + if ((xtop = xml_new("rpc", NULL, NULL)) == NULL) + goto done; + xbot = xtop; + /* Here xtop is: */ + if ((username = clicon_username_get(h)) != NULL){ + if ((xa = xml_new("username", xtop, NULL)) == NULL) + goto done; + xml_type_set(xa, CX_ATTR); + if (xml_value_set(xa, username) < 0) + goto done; + /* Here xtop is: */ + } + if ((ret = api_path2xml(oppath, yspec, xtop, YC_SCHEMANODE, 1, &xbot, &y)) < 0) + goto done; + if (ret == 0){ /* validation failed */ + if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + goto done; + clicon_err_reset(); + if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto done; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Here xtop is: + * xbot is + * 4. Parse input data (arguments): + * JSON: {"example:input":{"x":0}} + * XML: 0 + */ + namespace = xml_find_type_value(xbot, NULL, "xmlns", CX_ATTR); + clicon_debug(1, "%s : 4. Parse input data: %s", __FUNCTION__, data); + if (data && strlen(data)){ + if ((ret = api_operations_post_input(h, r, data, yspec, yrpc, xbot, + pretty, use_xml, parse_xml)) < 0) + goto done; + if (ret == 0) + goto ok; + } + /* Here xtop is: + 42 */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 5. Translate input args: %s", + __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* 6. Validate incoming RPC and fill in defaults */ + if (xml_spec_populate_rpc(h, xtop, yspec) < 0) /* */ + goto done; + if ((ret = xml_yang_validate_rpc(h, xtop, &xret)) < 0) + goto done; + if (ret == 0){ + if ((xe = xpath_first(xret, "rpc-error")) == NULL){ + clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); + goto ok; + } + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + /* Here xtop is (default values): + * 4299 + */ +#if 0 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xtop, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 6. Validate and defaults:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + /* 7. Send to RPC handler, either local or backend + * Note (1) xtop is xbot is + * (2) local handler wants and backend wants + */ + /* Look for local (client-side) restconf plugins. + * -1:Error, 0:OK local, 1:OK backend + */ + if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) + goto done; + if (ret > 0){ /* Handled locally */ + if (xml_parse_string(cbuf_get(cbret), NULL, &xret) < 0) + goto done; + /* Local error: return it and quit */ + if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } + else { /* Send to backend */ + if (clicon_rpc_netconf_xml(h, xtop, &xret, NULL) < 0) + goto done; + if ((xe = xpath_first(xret, "rpc-reply/rpc-error")) != NULL){ + if (api_return_err(h, r, xe, pretty, use_xml, 0) < 0) + goto done; + goto ok; + } + } + /* 8. Receive reply from local/backend handler as Netconf RPC + * 0 + */ +#if 1 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) + goto done; + clicon_debug(1, "%s 8. Receive reply:%s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + youtput = yang_find(yrpc, Y_OUTPUT, NULL); + if ((ret = api_operations_post_output(h, r, xret, yspec, youtput, namespace, + pretty, use_xml, &xoutput)) < 0) + goto done; + if (ret == 0) + goto ok; + /* xoutput should now look: 0 */ + FCGX_SetExitStatus(200, r->out); /* OK */ + FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json"); + FCGX_FPrintF(r->out, "\r\n"); + cbuf_reset(cbret); + if (use_xml){ + if (clicon_xml2cbuf(cbret, xoutput, 0, pretty) < 0) + goto done; + /* xoutput should now look: 0 */ + } + else{ + if (xml2json_cbuf(cbret, xoutput, pretty) < 0) + goto done; + /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ + } + FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); + FCGX_FPrintF(r->out, "\r\n\r\n"); + ok: + retval = 0; + done: + clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (prefix) + free(prefix); + if (id) + free(id); + if (xtop) + xml_free(xtop); + if (xret) + xml_free(xret); + if (xerr) + xml_free(xerr); + if (cbret) + cbuf_free(cbret); + return retval; +} diff --git a/apps/restconf/restconf_methods_post.h b/apps/restconf/restconf_methods_post.h new file mode 100644 index 00000000..cfb5e743 --- /dev/null +++ b/apps/restconf/restconf_methods_post.h @@ -0,0 +1,54 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2019 Olof Hagsand + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + * + * Restconf method implementation for post: operation(rpc) and data + */ + + +#ifndef _RESTCONF_METHODS_POST_H_ +#define _RESTCONF_METHODS_POST_H_ + +/* + * Prototypes + */ +int api_data_post(clicon_handle h, FCGX_Request *r, char *api_path, + cvec *pcvec, int pi, + cvec *qvec, char *data, + int pretty, int use_xml, int parse_xml); + +int api_operations_post(clicon_handle h, FCGX_Request *r, + char *path, + cvec *pcvec, int pi, cvec *qvec, char *data, + int pretty, int use_xml, int parse_xml); + +#endif /* _RESTCONF_METHODS_POST_H_ */ diff --git a/apps/restconf/restconf_stream.c b/apps/restconf/restconf_stream.c index 5c633347..3b33d506 100644 --- a/apps/restconf/restconf_stream.c +++ b/apps/restconf/restconf_stream.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. @@ -275,6 +275,7 @@ restconf_stream(clicon_handle h, } /* Setting up stream */ FCGX_SetExitStatus(201, r->out); /* Created */ + FCGX_FPrintF(r->out, "Status: 201 Created\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/event-stream\r\n"); FCGX_FPrintF(r->out, "Cache-Control: no-cache\r\n"); FCGX_FPrintF(r->out, "Connection: keep-alive\r\n"); @@ -368,7 +369,7 @@ api_stream(clicon_handle h, path = FCGX_GetParam("DOCUMENT_URI", r->envp); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - test(r, 1); + restconf_test(r, 1); if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) goto done; /* Sanity check of path. Should be /stream/ */ diff --git a/apps/restconf/restconf_stream.h b/apps/restconf/restconf_stream.h index 0cea952f..8c600a50 100644 --- a/apps/restconf/restconf_stream.h +++ b/apps/restconf/restconf_stream.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/clixon/clixon_xml_map.h b/lib/clixon/clixon_xml_map.h index 6730b02d..d8a9423e 100644 --- a/lib/clixon/clixon_xml_map.h +++ b/lib/clixon/clixon_xml_map.h @@ -77,6 +77,7 @@ int api_path2xml(char *api_path, yang_stmt *yspec, cxobj *xtop, yang_class nodeclass, int strict, cxobj **xpathp, yang_stmt **ypathp); int xml2xpath(cxobj *x, char **xpath); +int xml2api_path_1(cxobj *x, cbuf *cb); int xml_merge(cxobj *x0, cxobj *x1, yang_stmt *yspec, char **reason); int yang_enum_int_value(cxobj *node, int32_t *val); diff --git a/lib/clixon/clixon_xpath.h b/lib/clixon/clixon_xpath.h index ece3ecdb..c8a18b0e 100644 --- a/lib/clixon/clixon_xpath.h +++ b/lib/clixon/clixon_xpath.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/clixon/clixon_xpath_ctx.h b/lib/clixon/clixon_xpath_ctx.h index 0a1bb122..1a931da0 100644 --- a/lib/clixon/clixon_xpath_ctx.h +++ b/lib/clixon/clixon_xpath_ctx.h @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index c3175dd5..df56244f 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1576,22 +1576,28 @@ cvec2xml_1(cvec *cvv, } /*! Recursive help function to compute differences between two xml trees - * @param[in] x0 First XML tree - * @param[in] x1 Second XML tree - * @param[out] x0vec Pointervector to XML nodes existing in only first tree - * @param[out] x0veclen Length of first vector - * @param[out] x1vec Pointervector to XML nodes existing in only second tree - * @param[out] x1veclen Length of x1vec vector - * @param[out] changed_x0 Pointervector to XML nodes changed orig value - * @param[out] changed_x1 Pointervector to XML nodes changed wanted value + * @param[in] x0 First XML tree + * @param[in] x1 Second XML tree + * @param[out] x0vec Pointervector to XML nodes existing in only first tree + * @param[out] x0veclen Length of first vector + * @param[out] x1vec Pointervector to XML nodes existing in only second tree + * @param[out] x1veclen Length of x1vec vector + * @param[out] changed_x0 Pointervector to XML nodes changed orig value + * @param[out] changed_x1 Pointervector to XML nodes changed wanted value * @param[out] changedlen Length of changed vector * Algorithm to compare two sorted lists A, B: - * A 0 1 2 3 5 6 - * B 0 2 4 5 6 - * Let a,b be first elements of A,B respectively - * a = b : recurse; get next a,b - * a < b : add a in x0, get next a - * a > b : add b in x1, get next b + * A 0 1 2 3 5 6 + * B 0 2 4 5 6 + * Let (a, b) be first elements of (A, B) respectively(*) + * a = b : EITHER leafs: a!=b : add a in changed_x0, b in changed_x1, + * OR: Set (A,B) to children of (a,b) and call algorithm recursively + * , get next (a,b) + * a < b : add a in x0, get next a + * a > b : add b in x1, get next b + * (*) "comparing" a&b here is made by xml_cmp() which judges equality from a structural + * perspective, ie both have the same yang spec, if they are lists, they have the + * the same keys. NOT that the values are equal! + * @see xml_diff API function, this one is internal and recursive */ static int xml_diff1(yang_stmt *ys, @@ -1689,18 +1695,17 @@ xml_diff1(yang_stmt *ys, } /*! Compute differences between two xml trees - * @param[in] yspec Yang specification - * @param[in] x0 First XML tree - * @param[in] x1 Second XML tree - * @param[out] first Pointervector to XML nodes existing in only first tree - * @param[out] firstlen Length of first vector - * @param[out] second Pointervector to XML nodes existing in only second tree - * @param[out] secondlen Length of second vector - * @param[out] changed_x0 Pointervector to XML nodes changed orig value - * @param[out] changed_x1 Pointervector to XML nodes changed wanted value + * @param[in] yspec Yang specification + * @param[in] x0 First XML tree + * @param[in] x1 Second XML tree + * @param[out] first Pointervector to XML nodes existing in only first tree + * @param[out] firstlen Length of first vector + * @param[out] second Pointervector to XML nodes existing in only second tree + * @param[out] secondlen Length of second vector + * @param[out] changed_x0 Pointervector to XML nodes changed orig value + * @param[out] changed_x1 Pointervector to XML nodes changed wanted value * @param[out] changedlen Length of changed vector * All xml vectors should be freed after use. - * Bot xml trees should be freed with xml_free() */ int xml_diff(yang_stmt *yspec, @@ -1750,7 +1755,9 @@ xml_diff(yang_stmt *yspec, * @param[in] ys Yang statement * @param[in] inclkey If set include key leaf (eg last leaf d in ex) * @param[out] cb api_path_fmt, - * "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3 + * @retval 0 OK + * @retval -1 Error + * @see RFC8040 3.5.3 where "api-path" is defined as "URI-encoded path expression" */ static int yang2api_path_fmt_1(yang_stmt *ys, @@ -2819,12 +2826,14 @@ xml2xpath1(cxobj *x, cprintf(cb, "/%s", xml_name(x)); if ((y = xml_spec(x)) != NULL){ keyword = yang_keyword_get(y); - if (keyword == Y_LEAF_LIST){ + switch (keyword){ + case Y_LEAF_LIST: if ((b = xml_body(x)) != NULL) cprintf(cb, "[.=\"%s\"]", b); else cprintf(cb, "[.=\"\"]"); - } else if (keyword == Y_LIST){ + break; + case Y_LIST: cvk = yang_cvec_get(y); cvi = NULL; while ((cvi = cvec_each(cvk, cvi)) != NULL) { @@ -2836,6 +2845,9 @@ xml2xpath1(cxobj *x, b = xml_body(xb); cprintf(cb, "[%s=\"%s\"]", keyname, b?b:""); } + break; + default: + break; } } retval = 0; @@ -2866,25 +2878,6 @@ xml2xpath(cxobj *x, goto done; /* XXX: see xpath in test statement,.. */ xpath = cbuf_get(cb); -#if 0 /* debug test */ - { - cxobj *xt = x; - cxobj *xcp; - cxobj *x2; - while (xml_parent(xt) != NULL && - xml_spec(xt) != NULL) - xt = xml_parent(xt); - xcp = xml_parent(xt); - xml_parent_set(xt, NULL); - x2 = xpath_first(xt, "%s", xpath); /* +1: skip first / */ - xml_parent_set(xt, xcp); - assert(x2 && x==x2); - if (x==x2) - clicon_debug(1, "%s %s match", __FUNCTION__, xpath); - else - clicon_debug(1, "%s %s no match", __FUNCTION__, xpath); - } -#endif if (xpathp){ if ((*xpathp = strdup(xpath)) == NULL){ clicon_err(OE_UNIX, errno, "strdup"); @@ -2899,6 +2892,94 @@ xml2xpath(cxobj *x, return retval; } +/*! Construct an api_path from an XML node (single level not recursive) + * @param[in] x XML node (need to be yang populated) + * @param[out] cb api_path, must be initialized + * @retval 0 OK + * @retval -1 Error + * @see yang2api_path_fmt + * @see xml2xpath + */ +int +xml2api_path_1(cxobj *x, + cbuf *cb) +{ + int retval = -1; + yang_stmt *y = NULL; + cvec *cvk = NULL; /* vector of index keys */ + cg_var *cvi; + enum rfc_6020 keyword; + int i; + char *keyname; + cxobj *xkey; + cxobj *xb; + char *b; + char *enc; + yang_stmt *ymod; + cxobj *xp; + + if ((y = xml_spec(x)) == NULL){ + cprintf(cb, "/%s", xml_name(x)); + goto ok; + } + ymod = ys_module(y); + xp = xml_parent(x); + if (ymod && xp && xml_spec(xp)==NULL) /* Add prefix only if root */ + cprintf(cb, "/%s:%s", yang_argument_get(ymod), xml_name(x)); + else + cprintf(cb, "/%s", xml_name(x)); + keyword = yang_keyword_get(y); + switch (keyword){ + case Y_LEAF_LIST: + b = xml_body(x); + enc = NULL; + if (uri_percent_encode(&enc, "%s", b) < 0) + goto done; + cprintf(cb, "=%s", enc?enc:""); + if (enc) + free(enc); + break; + case Y_LIST: + cvk = y->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */ + if (cvec_len(cvk)) + cprintf(cb, "="); + /* Iterate over individual keys */ + cvi = NULL; + i = 0; + while ((cvi = cvec_each(cvk, cvi)) != NULL) { + keyname = cv_string_get(cvi); + if ((xkey = xml_find(x, keyname)) == NULL) + goto done; /* No key in xml */ + if ((xb = xml_find(x, keyname)) == NULL) + goto done; + if (i++) + cprintf(cb, ","); + b = xml_body(xb); + enc = NULL; + if (uri_percent_encode(&enc, "%s", b) < 0) + goto done; + cprintf(cb, "%s", enc?enc:""); + if (enc) + free(enc); + } + break; + default: + break; + } +#if 0 + { /* Just for testing */ + cxobj *xc; + if ((xc = xml_child_i_type(x, 0, CX_ELMNT)) != NULL) + if (xml2api_path_1(xc, cb) < 0) + goto done; + } +#endif + ok: + retval = 0; + done: + return retval; +} + /*! Check if the module tree x is in is assigned right XML namespace, assign if not * @param[in] x XML node *(0. You should probably find the XML root and apply this function to that.) diff --git a/lib/src/clixon_xml_sort.c b/lib/src/clixon_xml_sort.c index 35be1a14..30a1f332 100644 --- a/lib/src/clixon_xml_sort.c +++ b/lib/src/clixon_xml_sort.c @@ -207,6 +207,15 @@ xml_child_spec(cxobj *x, * @note empty value/NULL is smallest value * @note some error cases return as -1 (qsort cant handle errors) * @note some error cases return as -1 (qsort cant handle errors) + * + * NOTE: "comparing" x1 and x2 here judges equality from a structural (model) + * perspective, ie both have the same yang spec, if they are lists, they have the + * the same keys. NOT that the values are equal! + * In other words, if x is a leaf with the same yang spec, 1 and 2 are + * "equal". + * If x is a list element (with key "k"), + * 42foo and 42bar are equal, + * but is not equal to 71bar */ int xml_cmp(cxobj *x1, @@ -299,10 +308,10 @@ xml_cmp(cxobj *x1, else{ if (xml_cv_cache(x1b, &cv1) < 0) /* error case */ goto done; - assert(cv1); + // assert(cv1); if (xml_cv_cache(x2b, &cv2) < 0) /* error case */ goto done; - assert(cv2); + // assert(cv2); if ((equal = cv_cmp(cv1, cv2)) != 0) goto done; } diff --git a/lib/src/clixon_xpath.c b/lib/src/clixon_xpath.c index 22bd7677..7d62b39e 100644 --- a/lib/src/clixon_xpath.c +++ b/lib/src/clixon_xpath.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/lib/src/clixon_xpath_ctx.c b/lib/src/clixon_xpath_ctx.c index fbffa5c5..30f44af6 100644 --- a/lib/src/clixon_xpath_ctx.c +++ b/lib/src/clixon_xpath_ctx.c @@ -2,7 +2,7 @@ * ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2009-2019 Olof Hagsand and Benny Holmgren + Copyright (C) 2009-2019 Olof Hagsand This file is part of CLIXON. diff --git a/test/test_nacm_default.sh b/test/test_nacm_default.sh index 8da722c4..c7ca696e 100755 --- a/test/test_nacm_default.sh +++ b/test/test_nacm_default.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Basic NACM default rulw without any groups +# Basic NACM default rule without any groups # Start from startup db # Magic line must be first in script (see README.md) diff --git a/test/test_nacm_module_write.sh b/test/test_nacm_module_write.sh index 8673b2ff..41dc1049 100755 --- a/test/test_nacm_module_write.sh +++ b/test/test_nacm_module_write.sh @@ -176,10 +176,6 @@ nacm # update | p/d | xp/dx | p/d # delete | p/d | xp/dx | p/d -#----------root -new "update root list default deny" -expecteq "$(curl -u wilma:bar -sS -H 'Content-Type: application/yang-data+xml' -X PUT http://localhost/restconf/data -d '42$RULES')" 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}} ' - # replace all, then must include NACM rules as well MSG="$RULES" new "update root list permit" diff --git a/test/test_restconf.sh b/test/test_restconf.sh index fd4aecbd..4f0c6b36 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -160,7 +160,7 @@ expecteq "$(curl -s -X GET http://localhost/restconf/data/clixon-example:state)" # Exact match new "restconf Add subtree eth/0/0 to datastore using POST" -expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST -H "Accept: application/yang-data+json" -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 'HTTP/1.1 201 Created' new "restconf Re-add subtree eth/0/0 which should give error" expectfn 'curl -s -X POST -d {"ietf-interfaces:interfaces":{"interface":{"name":"eth/0/0","type":"ex:eth","enabled":true}}} http://localhost/restconf/data' 0 '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"data-exists","error-severity":"error","error-message":"Data already exists; cannot create new resource"}}}' diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 9cabb0b2..18703884 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -7,7 +7,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi APPNAME=example cfg=$dir/conf.xml -fyang=$dir/restconf.yang +fyang=$dir/example-jukebox.yang fxml=$dir/initial.xml # example @@ -326,9 +326,28 @@ expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://l new "B.1.3. Retrieve the Server Capability Information" expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit' +new "B.2.1. Create New Data Resources (artist+json)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters" + +new "B.2.1. Create New Data Resources (album+xml)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 201 Created" "Location: http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light" + +new "B.2.1. Add Data Resources again (conflict - not in RFC)" +expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters -d 'Wasting Light2011')" 0 "HTTP/1.1 409 Conflict" + +new "4.5. PUT replace content" +# XXX should be: jbox:alternative --> example-jukebox:alternative +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"jbox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" + +new "4.5. PUT replace content (xml encoding)" +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" + +new "4.5. PUT create new" +# XXX should be: jbox:alternative --> example-jukebox:alternative +expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" + + if false; then # NYI -new "B.2.1. Create New Data Resources" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library -d '{"example-jukebox:artist":[{"name":"Foo Fighters"}]}')" 0 "HTTP/1.1 201 Created" "Location:" # etc new "B.2.2. Detect Datastore Resource Entity-Tag Change" new "B.2.3. Edit a Datastore Resource" diff --git a/test/test_submodule.sh b/test/test_submodule.sh index d64812b3..02dd3923 100755 --- a/test/test_submodule.sh +++ b/test/test_submodule.sh @@ -204,13 +204,13 @@ expecteof "$clixon_netconf -qf $cfg" 0 "]]>]]>" "^< # Now same with restconf new "restconf edit main" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:main":{"x":"foo","ext":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:main":{"x":"foo","ext":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf edit sub1" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub1":{"x":"foo","ext1":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub1":{"x":"foo","ext1":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf edit sub2" -expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 200 OK' +expectfn 'curl -s -i -X POST http://localhost/restconf/data -d {"main:sub2":{"x":"foo","ext2":"foo"}}' 0 'HTTP/1.1 201 Created' new "restconf check main/sub1/sub2 contents" expectfn "curl -s -X GET http://localhost/restconf/data" 0 '{"data":{"main:main":{"ext":"foo","x":"foo"},"main:sub1":{"ext1":"foo","x":"foo"},"main:sub2":{"ext2":"foo","x":"foo"}}}'