From 46b6a8008aa4fad266fab3696e7c7c5f2c0e8d89 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Thu, 8 Aug 2019 11:42:45 +0200 Subject: [PATCH] RESTCONF PATCH (plain patch) is supported according to RFC 8040 4.6.1 --- CHANGELOG.md | 1 + apps/restconf/Makefile.in | 1 - apps/restconf/restconf_lib.c | 45 ++-- apps/restconf/restconf_lib.h | 2 +- apps/restconf/restconf_main.c | 31 +-- apps/restconf/restconf_methods.c | 316 ++++++++++++++++++------- apps/restconf/restconf_methods.h | 9 +- apps/restconf/restconf_methods_get.c | 5 +- apps/restconf/restconf_methods_patch.c | 249 ------------------- apps/restconf/restconf_methods_patch.h | 49 ---- apps/restconf/restconf_methods_post.c | 38 ++- apps/restconf/restconf_methods_post.h | 5 +- test/jukebox.sh | 3 +- test/test_restconf.sh | 4 +- test/test_restconf_jukebox.sh | 91 ++++--- 15 files changed, 375 insertions(+), 474 deletions(-) delete mode 100644 apps/restconf/restconf_methods_patch.c delete mode 100644 apps/restconf/restconf_methods_patch.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0a84d3..fd54b4bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Major New features * Restconf RFC 8040 increased feature compliance + * RESTCONF PATCH (plain patch) is supported according to RFC 8040 4.6.1 * RESTCONF "insert" and "point" query parameters supported * Applies to ordered-by-user leaf and leaf-lists * RESTCONF PUT/POST erroneously returned 200 OK. Instead restconf now returns: diff --git a/apps/restconf/Makefile.in b/apps/restconf/Makefile.in index b2936775..2e9629ac 100644 --- a/apps/restconf/Makefile.in +++ b/apps/restconf/Makefile.in @@ -78,7 +78,6 @@ APPSRC = restconf_main.c APPSRC += restconf_methods.c APPSRC += restconf_methods_post.c APPSRC += restconf_methods_get.c -APPSRC += restconf_methods_patch.c APPSRC += restconf_stream.c APPOBJ = $(APPSRC:.c=.o) diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index c340b724..dd255f9f 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -171,6 +171,21 @@ restconf_media_int2str(restconf_media media) return clicon_int2str(http_media_map, media); } +/*! Return media_in from Content-Type, -1 if not found or unrecognized + */ +restconf_media +restconf_content_type(FCGX_Request *r) +{ + char *str; + restconf_media m; + + if ((str = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp)) == NULL) + return -1; + if ((m = restconf_media_str2int(str)) == -1) + return -1; + return m; +} + /*! HTTP error 400 * @param[in] r Fastcgi request handle */ @@ -179,9 +194,9 @@ restconf_badrequest(FCGX_Request *r) { char *path; - clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 400\r\n"); /* 400 bad request */ + FCGX_SetExitStatus(400, r->out); + FCGX_FPrintF(r->out, "Status: 400 Bad Request\r\n"); /* 400 bad request */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Clixon Bad request/h1>\n"); FCGX_FPrintF(r->out, "The requested URL %s or data is in some way badly formed.\n", @@ -197,9 +212,9 @@ restconf_unauthorized(FCGX_Request *r) { char *path; - clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 401\r\n"); /* 401 unauthorized */ + FCGX_SetExitStatus(401, r->out); + FCGX_FPrintF(r->out, "Status: 401 Unauthorized\r\n"); /* 401 unauthorized */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "access-denied\n"); FCGX_FPrintF(r->out, "The requested URL %s was unauthorized.\n", path); @@ -214,9 +229,9 @@ restconf_forbidden(FCGX_Request *r) { char *path; - clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 403\r\n"); /* 403 forbidden */ + FCGX_SetExitStatus(403, r->out); + FCGX_FPrintF(r->out, "Status: 403 Forbidden\r\n"); /* 403 forbidden */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Forbidden

\n"); FCGX_FPrintF(r->out, "The requested URL %s was forbidden.\n", path); @@ -231,10 +246,9 @@ restconf_notfound(FCGX_Request *r) { char *path; - clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 404\r\n"); /* 404 not found */ - + FCGX_SetExitStatus(404, r->out); + FCGX_FPrintF(r->out, "Status: 404 Not Found\r\n"); /* 404 not found */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Not Found

\n"); FCGX_FPrintF(r->out, "Not Found\n"); @@ -251,9 +265,9 @@ restconf_notacceptable(FCGX_Request *r) { char *path; - clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 406\r\n"); /* 406 not acceptible */ + FCGX_SetExitStatus(406, r->out); + FCGX_FPrintF(r->out, "Status: 406 Not Acceptable\r\n"); /* 406 not acceptible */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Not Acceptable

\n"); @@ -269,8 +283,8 @@ restconf_notacceptable(FCGX_Request *r) int restconf_conflict(FCGX_Request *r) { - clicon_debug(1, "%s", __FUNCTION__); - FCGX_FPrintF(r->out, "Status: 409\r\n"); /* 409 Conflict */ + FCGX_SetExitStatus(409, r->out); + FCGX_FPrintF(r->out, "Status: 409 Conflict\r\n"); /* 409 Conflict */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Data resource already exists

\n"); return 0; @@ -282,7 +296,6 @@ restconf_conflict(FCGX_Request *r) int restconf_unsupported_media(FCGX_Request *r) { - clicon_debug(1, "%s", __FUNCTION__); FCGX_SetExitStatus(415, r->out); FCGX_FPrintF(r->out, "Status: 415 Unsupported Media Type\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); @@ -300,7 +313,7 @@ restconf_internal_server_error(FCGX_Request *r) clicon_debug(1, "%s", __FUNCTION__); path = FCGX_GetParam("DOCUMENT_URI", r->envp); - FCGX_FPrintF(r->out, "Status: 500\r\n"); /* 500 internal server error */ + FCGX_FPrintF(r->out, "Status: 500 Internal Server Error\r\n"); /* 500 internal server error */ FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Internal server error when accessing %s

\n", path); return 0; @@ -313,7 +326,7 @@ int restconf_notimplemented(FCGX_Request *r) { clicon_debug(1, "%s", __FUNCTION__); - FCGX_FPrintF(r->out, "Status: 501\r\n"); + FCGX_FPrintF(r->out, "Status: 501 Not Implemented\r\n"); FCGX_FPrintF(r->out, "Content-Type: text/html\r\n\r\n"); FCGX_FPrintF(r->out, "

Not Implemented/h1>\n"); return 0; diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 436b5004..316a7ddc 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -62,7 +62,7 @@ const char *restconf_code2reason(int code); const restconf_media restconf_media_str2int(char *media); const char *restconf_media_int2str(restconf_media media); - +restconf_media restconf_content_type(FCGX_Request *r); int restconf_badrequest(FCGX_Request *r); int restconf_unauthorized(FCGX_Request *r); int restconf_forbidden(FCGX_Request *r); diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 8fb809cd..1adf6cc2 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -80,7 +80,6 @@ #include "restconf_methods.h" #include "restconf_methods_get.h" #include "restconf_methods_post.h" -#include "restconf_methods_patch.h" #include "restconf_stream.h" /* Command line options to be passed to getopt(3) */ @@ -113,7 +112,6 @@ api_data(clicon_handle h, cvec *qvec, char *data, int pretty, - restconf_media media_in, restconf_media media_out) { int retval = -1; @@ -129,11 +127,11 @@ api_data(clicon_handle h, else if (strcmp(request_method, "GET")==0) retval = api_data_get(h, r, pcvec, pi, qvec, pretty, media_out); else if (strcmp(request_method, "POST")==0) - retval = api_data_post(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); + retval = api_data_post(h, r, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "PUT")==0) - retval = api_data_put(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); + retval = api_data_put(h, r, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "PATCH")==0) - retval = api_data_patch(h, r, api_path, pcvec, pi, qvec, data, pretty, media_in, media_out); + retval = api_data_patch(h, r, api_path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "DELETE")==0) retval = api_data_delete(h, r, api_path, pi, pretty, media_out); else @@ -150,7 +148,6 @@ api_data(clicon_handle h, * @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] media_in Input media media * @param[in] media_out Output media */ static int @@ -162,7 +159,6 @@ api_operations(clicon_handle h, cvec *qvec, char *data, int pretty, - restconf_media media_in, restconf_media media_out) { int retval = -1; @@ -175,7 +171,7 @@ api_operations(clicon_handle h, retval = api_operations_get(h, r, path, pcvec, pi, qvec, data, pretty, media_out); else if (strcmp(request_method, "POST")==0) retval = api_operations_post(h, r, path, pcvec, pi, qvec, data, - pretty, media_in, media_out); + pretty, media_out); else retval = restconf_notfound(r); return retval; @@ -334,7 +330,6 @@ api_restconf(clicon_handle h, char *data; int authenticated = 0; char *media_str = NULL; - restconf_media media_in = YANG_DATA_JSON; /* XXX defaults should be set in methods, here is too generic */ restconf_media media_out = YANG_DATA_JSON; int pretty; cbuf *cbret = NULL; @@ -345,19 +340,7 @@ api_restconf(clicon_handle h, path = restconf_uripath(r); query = FCGX_GetParam("QUERY_STRING", r->envp); pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); - /* Get media for input (Content-Type) - * This is for methods that have input, such as PUT/POST, etc - */ - if ((media_str = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp)) == NULL){ - retval = restconf_unsupported_media(r); - goto done; - } - else if ((media_in = restconf_media_str2int(media_str)) == -1){ - clicon_debug(1, "%s Content_Type: %s (unsupported)", __FUNCTION__, media_str); - retval = restconf_unsupported_media(r); - goto done; - } - clicon_debug(1, "%s CONTENT_TYPE: %s %s", __FUNCTION__, media_str, restconf_media_int2str(media_in)); + /* Get media for output (proactive negotiation) RFC7231 by using * Accept:. This is for methods that have output, such as GET, * operation POST, etc @@ -444,12 +427,12 @@ api_restconf(clicon_handle h, } else if (strcmp(method, "data") == 0){ /* restconf, skip /api/data */ if (api_data(h, r, path, pcvec, 2, qvec, data, - pretty, media_in, media_out) < 0) + pretty, media_out) < 0) goto done; } else if (strcmp(method, "operations") == 0){ /* rpc */ if (api_operations(h, r, path, pcvec, 2, qvec, data, - pretty, media_in, media_out) < 0) + pretty, media_out) < 0) goto done; } else if (strcmp(method, "test") == 0) diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 299e3341..200ea022 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -215,53 +215,24 @@ match_list_keys(yang_stmt *y, return retval; } -/*! Generic REST PUT 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] media_in Input media media - * @param[in] media_out Output media - - * @note restconf PUT is mapped to edit-config replace. - * @see RFC8040 Sec 4.5 PUT - * @see api_data_post - * @example - curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 - * - PUT: - 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, - FCGX_Request *r, - char *api_path0, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - restconf_media media_in, - restconf_media media_out) +/*! Common PUT plain PATCH method + * Code checks if object exists. + * PUT: If it does not, set op to create, otherwise replace + * PATCH: If it does not, fail, otherwise replace/merge + * @param[in] plain_patch fail if object does not exists AND merge (not replace) + */ +static int +api_data_write(clicon_handle h, + FCGX_Request *r, + char *api_path0, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + restconf_media media_in, + restconf_media media_out, + int plain_patch) { int retval = -1; enum operation_type op; @@ -282,13 +253,15 @@ api_data_put(clicon_handle h, 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; + cxobj *xerr = NULL; /* malloced must be freed */ + cxobj *xe; /* direct pointer into tree, dont free */ char *username; int ret; - char *namespace0; + char *namespace = NULL; char *dname; int nullspec = 0; + char *xpath = NULL; + cbuf *cbpath = NULL; clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path0); clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); @@ -299,6 +272,84 @@ api_data_put(clicon_handle h, api_path=api_path0; for (i=0; i", NETCONF_BASE_NAMESPACE); + cprintf(cbx, ">"); + if (namespace) + cprintf(cbx, "", + xpath, namespace); + else /* If xpath != /, this will probably yield an error later */ + cprintf(cbx, "", xpath); + cprintf(cbx, ""); + xret = NULL; + if (clicon_rpc_netconf(h, cbuf_get(cbx), &xret, NULL) < 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, media_out, 0) < 0) + goto done; + goto ok; + } +#if 0 + if (debug){ + cbuf *ccc=cbuf_new(); + if (clicon_xml2cbuf(ccc, xret, 0, 0) < 0) + goto done; + clicon_debug(1, "%s XRET: %s", __FUNCTION__, cbuf_get(ccc)); + cbuf_free(ccc); + } +#endif + if ((xe = xpath_first(xret, "/rpc-reply/data")) == NULL || + xml_child_nr(xe) == 0){ /* Object does not exist */ + if (plain_patch){ /* If the target resource instance does not exist, the server MUST NOT create it. */ + restconf_badrequest(r); + goto ok; + } + else + op = OP_CREATE; + } + else{ + if (plain_patch) + op = OP_MERGE; + else + op = OP_REPLACE; + } + if (xret){ + xml_free(xret); + xret = NULL; + } + /* Create config top-of-tree */ if ((xtop = xml_new("config", NULL, NULL)) == NULL) goto done; @@ -322,6 +373,20 @@ api_data_put(clicon_handle h, goto ok; } } + /* 4.4.1: The message-body MUST contain exactly one instance of the + * expected data resource. (tested again below) + */ + if (data == NULL || strlen(data) == 0){ + if (netconf_malformed_message_xml(&xerr, "The message-body MUST contain exactly one instance of the expected data resource") < 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, media_out, 0) < 0) + goto done; + goto ok; + } /* Parse input data as json or xml into xml */ switch (media_in){ @@ -365,13 +430,18 @@ api_data_put(clicon_handle h, goto done; goto ok; } + break; + default: + restconf_unsupported_media(r); + goto ok; + break; } /* switch media_in */ /* The message-body MUST contain exactly one instance of the * expected data resource. */ if (xml_child_nr(xdata0) != 1){ - if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0) + if (netconf_malformed_message_xml(&xerr, "The message-body MUST contain exactly one instance of the expected data resource") < 0) goto done; if ((xe = xpath_first(xerr, "rpc-error")) == NULL){ clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)"); @@ -411,15 +481,17 @@ api_data_put(clicon_handle h, } } - /* Add operation create as attribute. If that fails with Conflict, then try - "replace" */ + /* Add operation create as attribute. If that fails with Conflict, then + * try "replace" (see comment in function header) + */ if ((xa = xml_new("operation", xdata, NULL)) == NULL) goto done; xml_type_set(xa, CX_ATTR); xml_prefix_set(xa, NETCONF_BASE_PREFIX); - op = OP_CREATE; + if (xml_value_set(xa, xml_operation2str(op)) < 0) goto done; + /* Top-of tree, no api-path * Replace xparent with x, ie bottom of api-path with data */ @@ -529,21 +601,17 @@ api_data_put(clicon_handle h, /* If we already have that default namespace, remove it in child */ if ((xa = xml_find_type(xdata, NULL, "xmlns", CX_ATTR)) != NULL){ - if (xml2ns(xparent, NULL, &namespace0) < 0) + if (xml2ns(xparent, NULL, &namespace) < 0) goto done; /* Set xmlns="" default namespace attribute (if diff from default) */ - if (strcmp(namespace0, xml_value(xa))==0) + if (strcmp(namespace, xml_value(xa))==0) xml_purge(xa); } - } - /* Create text buffer for transfer to backend */ - if ((cbx = cbuf_new()) == NULL) - goto done; /* For internal XML protocol: add username attribute for access control */ username = clicon_username_get(h); - again: + cbuf_reset(cbx); cprintf(cbx, "", username?username:"", NETCONF_BASE_PREFIX, @@ -557,30 +625,9 @@ 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 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, media_out, 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; - } - 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; + if (api_return_err(h, r, xe, pretty, media_out, 0) < 0) + goto done; + goto ok; } cbuf_reset(cbx); /* commit/discard should be done automaticaly by the system, therefore @@ -640,6 +687,8 @@ api_data_put(clicon_handle h, retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); + if (cbpath) + cbuf_reset(cbpath); if (xret) xml_free(xret); if (xerr) @@ -657,6 +706,103 @@ api_data_put(clicon_handle h, return retval; } /* api_data_put */ + +/*! Generic REST PUT 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] media_out Output media + + * @note restconf PUT is mapped to edit-config replace. + * @see RFC8040 Sec 4.5 PUT + * @see api_data_post + * @example + curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 + * + PUT: + 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, + FCGX_Request *r, + char *api_path0, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + restconf_media media_out) +{ + restconf_media media_in; + + media_in = restconf_content_type(r); + return api_data_write(h, r, api_path0, pcvec, pi, qvec, data, pretty, + media_in, media_out, 0); +} + +/*! Generic REST PATCH method for plain patch + * @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] media_out Output media + * Netconf: (nc:operation="merge") + * See RFC8040 Sec 4.6.1 + * Plain patch can be used to create or update, but not delete, a child + * resource within the target resource. + * NOTE: If the target resource instance does not exist, the server MUST NOT + * create it. (CANT BE DONE WITH NETCONF) + */ +int +api_data_patch(clicon_handle h, + FCGX_Request *r, + char *api_path0, + cvec *pcvec, + int pi, + cvec *qvec, + char *data, + int pretty, + restconf_media media_out) +{ + restconf_media media_in; + int ret; + + media_in = restconf_content_type(r); + if (media_in == YANG_DATA_XML || media_in == YANG_DATA_JSON){ + /* plain patch */ + ret = api_data_write(h, r, api_path0, pcvec, pi, qvec, data, pretty, + media_in, media_out, 1); + } + else{ /* Other patches are NYI */ + ret = restconf_notimplemented(r); + } + return ret; +} + /*! Generic REST DELETE method translated to edit-config * @param[in] h CLIXON handle * @param[in] r Fastcgi request handle diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index a1859fb1..922ee2f9 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -45,8 +45,13 @@ int api_data_options(clicon_handle h, FCGX_Request *r); int api_data_put(clicon_handle h, FCGX_Request *r, char *api_path, cvec *pcvec, int pi, cvec *qvec, char *data, - int pretty, - restconf_media media_in, restconf_media media_out); + int pretty, restconf_media media_out); + +int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path, + cvec *pcvec, int pi, + cvec *qvec, char *data, int pretty, + restconf_media media_out); + int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi, int pretty, restconf_media media_out); diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index ecd2cd1a..d8108185 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -116,7 +116,10 @@ api_data_get2(clicon_handle h, cvec *nsc = NULL; clicon_debug(1, "%s", __FUNCTION__); - yspec = clicon_dbspec_yang(h); + if ((yspec = clicon_dbspec_yang(h)) == NULL){ + clicon_err(OE_FATAL, 0, "No DB_SPEC"); + goto done; + } if ((cbpath = cbuf_new()) == NULL) goto done; cprintf(cbpath, "/"); diff --git a/apps/restconf/restconf_methods_patch.c b/apps/restconf/restconf_methods_patch.c deleted file mode 100644 index 48338198..00000000 --- a/apps/restconf/restconf_methods_patch.c +++ /dev/null @@ -1,249 +0,0 @@ -/* - * - ***** BEGIN LICENSE BLOCK ***** - - Copyright (C) 2009-2019 Olof Hagsand - - This file is part of CLIXON. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the "GPL"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK ***** - - */ - -/* - * See rfc8040 - - * sudo apt-get install libfcgi-dev - * gcc -o fastcgi fastcgi.c -lfcgi - - * sudo su -c "/www-data/clixon_restconf -D 1 f /usr/local/etc/example.xml " -s /bin/sh www-data - - * This is the interface: - * api/data/profile=/metric= PUT data:enable= - * api/test - +----------------------------+--------------------------------------+ - | 100 Continue | POST accepted, 201 should follow | - | 200 OK | Success with response message-body | - | 201 Created | POST to create a resource success | - | 204 No Content | Success without response message- | - | | body | - | 304 Not Modified | Conditional operation not done | - | 400 Bad Request | Invalid request message | - | 401 Unauthorized | Client cannot be authenticated | - | 403 Forbidden | Access to resource denied | - | 404 Not Found | Resource target or resource node not | - | | found | - | 405 Method Not Allowed | Method not allowed for target | - | | resource | - | 409 Conflict | Resource or lock in use | - | 412 Precondition Failed | Conditional method is false | - | 413 Request Entity Too | too-big error | - | Large | | - | 414 Request-URI Too Large | too-big error | - | 415 Unsupported Media Type | non RESTCONF media type | - | 500 Internal Server Error | operation-failed | - | 501 Not Implemented | unknown-operation | - | 503 Service Unavailable | Recoverable server error | - +----------------------------+--------------------------------------+ -Mapping netconf error-tag -> status code - +-------------------------+-------------+ - | | status code | - +-------------------------+-------------+ - | in-use | 409 | - | invalid-value | 400 | - | too-big | 413 | - | missing-attribute | 400 | - | bad-attribute | 400 | - | unknown-attribute | 400 | - | bad-element | 400 | - | unknown-element | 400 | - | unknown-namespace | 400 | - | access-denied | 403 | - | lock-denied | 409 | - | resource-denied | 409 | - | rollback-failed | 500 | - | data-exists | 409 | - | data-missing | 409 | - | operation-not-supported | 501 | - | operation-failed | 500 | - | partial-operation | 500 | - | malformed-message | 400 | - +-------------------------+-------------+ - - * "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3 - */ - -#ifdef HAVE_CONFIG_H -#include "clixon_config.h" /* generated by config & autoconf */ -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* cligen */ -#include - -/* clicon */ -#include - -#include /* Need to be after clixon_xml-h due to attribute format */ - -#include "restconf_lib.h" -#include "restconf_methods_patch.h" - - -/*! Generic REST PATCH method for plain patch - * @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] media_out Output media - * Netconf: (nc:operation="merge") - * See RFC8040 Sec 4.6.1 - */ -int -api_data_patch(clicon_handle h, - FCGX_Request *r, - char *api_path0, - cvec *pcvec, - int pi, - cvec *qvec, - char *data, - int pretty, - restconf_media media_in, - restconf_media media_out) -{ - int retval = -1; - yang_stmt *yspec; - // yang_stmt *ymodapi = NULL; /* yang module of api-path (if any) */ - cxobj *xdata0 = NULL; /* Original -d data struct (including top symbol) */ - int i; - char *api_path; - cxobj *xtop = NULL; /* top of api-path */ - cxobj *xbot = NULL; /* bottom of api-path */ - yang_stmt *ybot = NULL; /* yang of xbot */ - cxobj *xerr = NULL; /* malloced must be freed */ - cxobj *xe; - int ret; - - clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path0); - clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data); - if ((yspec = clicon_dbspec_yang(h)) == NULL){ - clicon_err(OE_FATAL, 0, "No DB_SPEC"); - goto done; - } - api_path=api_path0; - for (i=0; i $fjukebox module example-jukebox { diff --git a/test/test_restconf.sh b/test/test_restconf.sh index c266f627..068c6f56 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -202,8 +202,8 @@ expectpart "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d ' new "Add leaf description using POST" expecteq "$(curl -s -X POST -H "Content-Type: application/yang-data+json" -d '{"ietf-interfaces:description":"The-first-interface"}' http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 "" -new "Add nothing using POST" -expectfn 'curl -s -X POST http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0' 0 '"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":" on line 1: syntax error at or before:' +new "Add nothing using POST (expect fail)" +expectpart "$(curl -is -X POST -H "Content-Type: application/yang-data+json" http://localhost/restconf/data/ietf-interfaces:interfaces/interface=eth%2f0%2f0)" 0 'HTTP/1.1 400 Bad Request' '{"ietf-restconf:errors":{"error":{"error-type":"rpc","error-tag":"malformed-message","error-severity":"error","error-message":"The message-body MUST contain exactly one instance of the expected data resource"}}}' new "restconf Check description added" expecteq "$(curl -s -G http://localhost/restconf/data/ietf-interfaces:interfaces)" 0 '{"ietf-interfaces:interfaces":{"interface":[{"name":"eth/0/0","description":"The-first-interface","type":"clixon-example:eth","enabled":true,"oper-status":"up"}]}} diff --git a/test/test_restconf_jukebox.sh b/test/test_restconf_jukebox.sh index 88e78b28..e14e6442 100755 --- a/test/test_restconf_jukebox.sh +++ b/test/test_restconf_jukebox.sh @@ -9,13 +9,26 @@ APPNAME=example cfg=$dir/conf.xml fjukebox=$dir/example-jukebox.yang +# A "system" module as defined in B.2.4 +cat < $dir/example-system.yang + module example-system { + namespace "http://example.com/ns/example-system"; + prefix "ex"; + container system { + leaf enable-jukebox-streaming { + type boolean; + } + } + } +EOF + # example cat < $cfg $cfg /usr/local/share/clixon $IETFRFC - $fjukebox + $dir false /usr/local/var/$APPNAME/$APPNAME.sock /usr/local/lib/$APPNAME/backend @@ -52,102 +65,120 @@ wait_backend wait_restconf new "B.1.1. Retrieve the Top-Level API Resource root" -expectpart "$(curl -s -i -X GET -H 'Accept: application/xrd+xml' http://localhost/.well-known/host-meta)" 0 "HTTP/1.1 200 OK" "Content-Type: application/xrd+xml" "" "" "" +expectpart "$(curl -si -X GET -H 'Accept: application/xrd+xml' http://localhost/.well-known/host-meta)" 0 "HTTP/1.1 200 OK" "Content-Type: application/xrd+xml" "" "" "" d='{"ietf-restconf:restconf":{"data":{},"operations":{},"yang-library-version":"2016-06-21"}}' new "B.1.1. Retrieve the Top-Level API Resource /restconf json" -expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" "$d" +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" "$d" new "B.1.1. Retrieve the Top-Level API Resource /restconf xml (not in RFC)" -expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+xml" '2016-06-21' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+xml" '2016-06-21' # This just catches the header and the jukebox module, the RFC has foo and bar which # seems wrong to recreate new "B.1.2. Retrieve the Server Module Information" -expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":' '"module":\[{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"}' +expectpart "$(curl -si -X GET -H 'Accept: application/yang-data+json' http://localhost/restconf/data/ietf-yang-library:modules-state)" 0 "HTTP/1.1 200 OK" 'Cache-Control: no-cache' "Content-Type: application/yang-data+json" '{"ietf-yang-library:modules-state":{"module-set-id":' '"module":\[{"name":"example-jukebox","revision":"2016-08-15","namespace":"http://example.com/ns/example-jukebox","conformance-type":"implement"}' new "B.1.3. Retrieve the Server Capability Information" -expectpart "$(curl -s -i -X GET -H 'Accept: application/yang-data+xml' http://localhost/restconf/data/ietf-restconf-monitoring:restconf-state/capabilities)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+xml" 'Cache-Control: no-cache' 'urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit' +expectpart "$(curl -si -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" +expectpart "$(curl -si -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" +expectpart "$(curl -si -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" +expectpart "$(curl -si -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" -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" + +new "B.2.2. Added genre" +# Here the jbox identity is added +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d '{"example-jukebox:album":[{"name":"Wasting Light","genre":"example-jukebox:alternative","year":2011}]}')" 0 "HTTP/1.1 204 No Content" new "4.5. PUT replace content (xml encoding)" -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light -d 'Wasting Lightjbox:alternative2011')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -si -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 identity" -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d '{"example-jukebox:album":[{"name":"London Calling","year":1979}]}')" 0 "HTTP/1.1 201 Created" -new "restconf DELETE whole datastore" -expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" +new "4.5. Check jukebox content: 1 Clash and 1 Foo fighters album" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersWasting Lightjbox:alternative2011' + +# First use of PATCH +new "B.2.2. Detect Datastore Resource Entity-Tag Change (XXX if-unmodified)" +expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album=Wasting%20Light/genre -d '{"example-jukebox:genre":"example-jukebox:alternative"}')" 0 'HTTP/1.1 204 No Content' + +new "B.2.3. Edit a Datastore Resource (Add 1 Foo fighter and Nick cave album)" +expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'trueFoo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 'HTTP/1.1 204 No Content' + +new "B.2.3. Check patch system" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-system:system -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'true' + +new "B.2.3. Check jukebox: 1 Clash, 2 Foo Fighters, 1 Nick Cave" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Calling1979Foo FightersOne by One2012Wasting Lightalternative2011Nick Cave and the Bad SeedsTender Prey1988' new "B.2.4. Replace a Datastore Resource" -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988')" 0 "HTTP/1.1 204 No Content" + +new "B.2.4. Check replace" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Foo FightersOne by One2012Nick Cave and the Bad SeedsTender Prey1988' new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new 'B.3.4. "insert" Parameter' JSON="{\"example-jukebox:song\":[{\"index\":1,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Rope']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=1' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=1' new 'B.3.4. "insert" Parameter first (RFC example says after)' JSON="{\"example-jukebox:song\":[{\"index\":0,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=0' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=first -d "$JSON")" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=0' new 'B.3.4. "insert" Parameter check order' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new 'B.3.5. "point" Parameter (before for more interesting order: 0,2,1)' JSON="{\"example-jukebox:song\":[{\"index\":2,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Bridge Burning']\"}]}" -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One?insert=before\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D1 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=2' new 'B.3.5. "point" check order (0,2,1)' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new 'B.3.5. "point" Parameter 3 after 2 (using PUT)' JSON="{\"example-jukebox:song\":[{\"index\":3,\"id\":\"/example-jukebox:jukebox/library/artist[name='Foo Fighters']/album[name='Wasting Light']/song[name='Something else']\"}]}" -expectpart "$(curl -s -i -X PUT -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=3?insert=after\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D2 )" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' -d "$JSON" http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One/song=3?insert=after\&point=%2Fexample-jukebox%3Ajukebox%2Fplaylist%3DFoo-One%2Fsong%3D2 )" 0 "HTTP/1.1 201 Created" new 'B.3.5. "point" check order (0,2,3,1)' RES="Foo-One0/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]2/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]3/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Something else'\]1/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]" -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/playlist=Foo-One -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' "$RES" new "restconf DELETE whole datastore" expectfn 'curl -s -X DELETE http://localhost/restconf/data' 0 "" new 'B.3.4. "insert/point" leaf-list 3 (not in RFC)' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=3' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"3"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=3' new 'B.3.4. "insert/point" leaf-list 2 first' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=2' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=first -d '{"example-jukebox:extra":"2"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=2' new 'B.3.4. "insert/point" leaf-list 1 last' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' #new 'B.3.4. "insert/point" move leaf-list 1 last' #- restconf cannot move a leaf-list(list?) item -#expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' +#expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data?insert=last -d '{"example-jukebox:extra":"1"}')" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=1' new 'B.3.5. "insert/point" leaf-list check order (2,3,1)' -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '231' new 'B.3.5. "point" Parameter leaf-list 4 before 3' -expectpart "$(curl -s -i -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=4' +expectpart "$(curl -si -X POST -H 'Content-Type: application/yang-data+json' -d '{"example-jukebox:extra":"4"}' http://localhost/restconf/data?insert=before\&point=%2Fexample-jukebox%3Aextra%3D3 )" 0 "HTTP/1.1 201 Created" 'Location: http://localhost/restconf/data/example-jukebox:extra=4' new 'B.3.5. "insert/point" leaf-list check order (2,4,3,1)' -expectpart "$(curl -s -i -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431' +expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '2431' if false; then # NYI