RESTCONF PATCH (plain patch) is supported according to RFC 8040 4.6.1

This commit is contained in:
Olof hagsand 2019-08-08 11:42:45 +02:00
parent bb2eaa3cc7
commit 46b6a8008a
15 changed files with 375 additions and 474 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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, "<h1>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, "<error-tag>access-denied</error-tag>\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, "<h1>Forbidden</h1>\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, "<h1>Not Found</h1>\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, "<h1>Not Acceptable</h1>\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, "<h1>Data resource already exists</h1>\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, "<h1>Internal server error when accessing %s</h1>\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, "<h1>Not Implemented/h1>\n");
return 0;

View file

@ -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);

View file

@ -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)

View file

@ -215,44 +215,14 @@ 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: <edit-config> (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 <ok/> 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
/*! 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)
*/
int
api_data_put(clicon_handle h,
static int
api_data_write(clicon_handle h,
FCGX_Request *r,
char *api_path0,
cvec *pcvec,
@ -261,7 +231,8 @@ api_data_put(clicon_handle h,
char *data,
int pretty,
restconf_media media_in,
restconf_media media_out)
restconf_media media_out,
int plain_patch)
{
int retval = -1;
enum operation_type op;
@ -283,12 +254,14 @@ api_data_put(clicon_handle h,
cxobj *xretcom = NULL; /* return from commit */
cxobj *xretdis = NULL; /* return from discard-changes */
cxobj *xerr = NULL; /* malloced must be freed */
cxobj *xe;
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<pi; i++)
api_path = index(api_path+1, '/');
/* Check if object exists in backend.
* Translate api-path to xpath */
namespace = NULL;
if ((cbpath = cbuf_new()) == NULL)
goto done;
cprintf(cbpath, "/");
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, media_out, 0) < 0)
goto done;
goto ok;
}
xpath = cbuf_get(cbpath);
/* Create text buffer for transfer to backend */
if ((cbx = cbuf_new()) == NULL)
goto done;
/* show done automaticaly by the system, therefore recovery user is used
* here */
cprintf(cbx, "<rpc username=\"%s\"", NACM_RECOVERY_USER);
if (namespace)
cprintf(cbx, " xmlns:nc=\"%s\">", NETCONF_BASE_NAMESPACE);
cprintf(cbx, "><get-config><source><candidate/></source>");
if (namespace)
cprintf(cbx, "<nc:filter nc:type=\"xpath\" nc:select=\"%s\" xmlns=\"%s\"/>",
xpath, namespace);
else /* If xpath != /, this will probably yield an error later */
cprintf(cbx, "<filter type=\"xpath\" select=\"%s\"/>", xpath);
cprintf(cbx, "</get-config></rpc>");
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, "<rpc username=\"%s\" xmlns:%s=\"%s\">",
username?username:"",
NETCONF_BASE_PREFIX,
@ -557,31 +625,10 @@ 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;
}
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
@ -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: <edit-config> (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 <ok/> 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: <edit-config> (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

View file

@ -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);

View file

@ -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, "/");

View file

@ -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=<name>/metric=<name> PUT data:enable=<flag>
* 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
+-------------------------+-------------+
| <error&#8209;tag> | 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 <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <syslog.h>
#include <fcntl.h>
#include <assert.h>
#include <time.h>
#include <signal.h>
#include <limits.h>
#include <sys/time.h>
#include <sys/wait.h>
/* cligen */
#include <cligen/cligen.h>
/* clicon */
#include <clixon/clixon.h>
#include <fcgiapp.h> /* 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: <edit-config> (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<pi; i++)
api_path = index(api_path+1, '/');
/* Create config top-of-tree */
if ((xtop = xml_new("config", NULL, NULL)) == NULL)
goto done;
/* Translate api_path to xml in the form of xtop/xbot */
xbot = xtop;
if (api_path){ /* If URI, otherwise top data/config object */
if ((ret = api_path2xml(api_path, yspec, xtop, YC_DATANODE, 1, &xbot, &ybot)) < 0)
goto done;
// if (ybot)
// ymodapi = ys_module(ybot);
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, media_out, 0) < 0)
goto done;
goto ok;
}
}
/* Parse input data as json or xml into xml */
switch (media_in){
case YANG_DATA_XML:
if (xml_parse_string(data, yspec, &xdata0) < 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, media_out, 0) < 0)
goto done;
goto ok;
}
break;
case YANG_DATA_JSON:
/* Data here cannot cannot be Yang populated since it is loosely
* hanging without top symbols.
* And if it is not yang populated, it cant be translated properly
* from JSON to XML.
* Therefore, yang population is done later after addsub below
*/
if ((ret = json_parse_str(data, yspec, &xdata0, &xerr)) < 0){
if (netconf_malformed_message_xml(&xerr, clicon_err_reason) < 0)
goto done;
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 (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, media_out, 0) < 0)
goto done;
goto ok;
}
} /* switch media_in */
restconf_notimplemented(r);
ok:
retval = 0;
done:
if (xtop)
xml_free(xtop);
return retval;
}

View file

@ -1,49 +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 *****
* Restconf method implementation
*/
#ifndef _RESTCONF_METHODS_PATCH_H_
#define _RESTCONF_METHODS_PATCH_H_
/*
* Prototypes
*/
int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path,
cvec *pcvec, int pi,
cvec *qvec, char *data, int pretty,
restconf_media media_in, restconf_media media_out);
#endif /* _RESTCONF_METHODS_PATCH_H_ */

View file

@ -73,7 +73,6 @@
* @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
* @param[in] media_out Output media
* restconf POST is mapped to edit-config create.
* @see RFC8040 Sec 4.4.1
@ -106,7 +105,6 @@ api_data_post(clicon_handle h,
cvec *qvec,
char *data,
int pretty,
restconf_media media_in,
restconf_media media_out)
{
int retval = -1;
@ -130,6 +128,7 @@ api_data_post(clicon_handle h,
char *username;
int nullspec = 0;
int ret;
restconf_media media_in;
clicon_debug(1, "%s api_path:\"%s\"", __FUNCTION__, api_path);
clicon_debug(1, "%s data:\"%s\"", __FUNCTION__, data);
@ -171,7 +170,22 @@ api_data_post(clicon_handle h,
cbuf_free(ccc);
}
#endif
/* 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 */
media_in = restconf_content_type(r);
switch (media_in){
case YANG_DATA_XML:
if (xml_parse_string(data, NULL, &xdata0) < 0){
@ -217,12 +231,16 @@ api_data_post(clicon_handle h,
goto ok;
}
break;
default:
restconf_unsupported_media(r);
goto ok;
break;
} /* switch media_in */
/* 4.4.1: 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)");
@ -401,7 +419,6 @@ api_data_post(clicon_handle h,
* @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] media_in Input media
* @param[in] media_out Output media
* @retval 1 OK
* @retval 0 Fail, Error message sent
@ -423,7 +440,6 @@ api_operations_post_input(clicon_handle h,
yang_stmt *yrpc,
cxobj *xrpc,
int pretty,
restconf_media media_in,
restconf_media media_out)
{
int retval = -1;
@ -434,6 +450,7 @@ api_operations_post_input(clicon_handle h,
cxobj *x;
cbuf *cbret = NULL;
int ret;
restconf_media media_in;
clicon_debug(1, "%s %s", __FUNCTION__, data);
if ((cbret = cbuf_new()) == NULL){
@ -441,6 +458,7 @@ api_operations_post_input(clicon_handle h,
goto done;
}
/* Parse input data as json or xml into xml */
media_in = restconf_content_type(r);
switch (media_in){
case YANG_DATA_XML:
if (xml_parse_string(data, yspec, &xdata) < 0){
@ -477,6 +495,10 @@ api_operations_post_input(clicon_handle h,
goto fail;
}
break;
default:
restconf_unsupported_media(r);
goto fail;
break;
} /* switch media_in */
xml_name_set(xdata, "data");
/* Here xdata is:
@ -676,7 +698,6 @@ api_operations_post_output(clicon_handle h,
* @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
* See RFC 8040 Sec 3.6 / 4.4.2
* @note We map post to edit-config create.
@ -708,7 +729,6 @@ api_operations_post(clicon_handle h,
cvec *qvec,
char *data,
int pretty,
restconf_media media_in,
restconf_media media_out)
{
int retval = -1;
@ -825,7 +845,7 @@ api_operations_post(clicon_handle h,
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, media_in, media_out)) < 0)
pretty, media_out)) < 0)
goto done;
if (ret == 0)
goto ok;

View file

@ -45,15 +45,12 @@ int api_data_post(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 api_operations_post(clicon_handle h, FCGX_Request *r,
char *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);
#endif /* _RESTCONF_METHODS_POST_H_ */

View file

@ -1,6 +1,7 @@
#!/bin/bash
# Jukebox example from rfc 8040 Appendix A.1
# ASsumes fjukebox is set to name of yang file
# Assumes fjukebox is set to name of yang file
# An extra leaf-list is added (clixon)
cat <<EOF > $fjukebox
module example-jukebox {

View file

@ -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"}]}}

View file

@ -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 <<EOF > $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
# <CLICON_YANG_MODULE_MAIN>example</CLICON_YANG_MODULE_MAIN>
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<CLICON_YANG_MAIN_FILE>$fjukebox</CLICON_YANG_MAIN_FILE>
<CLICON_YANG_MAIN_DIR>$dir</CLICON_YANG_MAIN_DIR>
<CLICON_RESTCONF_PRETTY>false</CLICON_RESTCONF_PRETTY>
<CLICON_SOCK>/usr/local/var/$APPNAME/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
@ -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" "<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'>" "<Link rel='restconf' href='/restconf'/>" "</XRD>"
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" "<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'>" "<Link rel='restconf' href='/restconf'/>" "</XRD>"
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" '<restconf xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><data/><operations/><yang-library-version>2016-06-21</yang-library-version></restconf>'
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" '<restconf xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><data/><operations/><yang-library-version>2016-06-21</yang-library-version></restconf>'
# 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' '<capabilities xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf-monitoring"><capability>urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit</capability></capabilities>'
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' '<capabilities xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf-monitoring"><capability>urn:ietf:params:restconf:capability:defaults:1.0?basic-mode=explicit</capability></capabilities>'
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 '<album xmlns="http://example.com/ns/example-jukebox"><name>Wasting Light</name><year>2011</year></album>')" 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 '<album xmlns="http://example.com/ns/example-jukebox"><name>Wasting Light</name><year>2011</year></album>')" 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 '<album xmlns="http://example.com/ns/example-jukebox"><name>Wasting Light</name><year>2011</year></album>')" 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 '<album xmlns="http://example.com/ns/example-jukebox"><name>Wasting Light</name><year>2011</year></album>')" 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 '<album xmlns="http://example.com/ns/example-jukebox" xmlns:jbox="http://example.com/ns/example-jukebox"><name>Wasting Light</name><genre>jbox:alternative</genre><year>2011</year></album>')" 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 '<album xmlns="http://example.com/ns/example-jukebox" xmlns:jbox="http://example.com/ns/example-jukebox"><name>Wasting Light</name><genre>jbox:alternative</genre><year>2011</year></album>')" 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' '<jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Clash</name><album><name>London Calling</name><year>1979</year></album></artist><artist><name>Foo Fighters</name><album xmlns:jbox="http://example.com/ns/example-jukebox"><name>Wasting Light</name><genre>jbox:alternative</genre><year>2011</year></album></artist></library></jukebox>'
# 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 '<data xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><system xmlns="http://example.com/ns/example-system"><enable-jukebox-streaming>true</enable-jukebox-streaming></system><jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Foo Fighters</name><album><name>One by One</name><year>2012</year></album></artist><artist><name>Nick Cave and the Bad Seeds</name><album><name>Tender Prey</name><year>1988</year></album></artist></library></jukebox></data>')" 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' '<system xmlns="http://example.com/ns/example-system"><enable-jukebox-streaming>true</enable-jukebox-streaming></system>'
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' '<jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Clash</name><album><name>London Calling</name><year>1979</year></album></artist><artist><name>Foo Fighters</name><album><name>One by One</name><year>2012</year></album><album xmlns:jbox="http://example.com/ns/example-jukebox"><name>Wasting Light</name><genre>alternative</genre><year>2011</year></album></artist><artist><name>Nick Cave and the Bad Seeds</name><album><name>Tender Prey</name><year>1988</year></album></artist></library></jukebox>'
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 '<data xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Foo Fighters</name><album><name>One by One</name><year>2012</year></album></artist><artist><name>Nick Cave and the Bad Seeds</name><album><name>Tender Prey</name><year>1988</year></album></artist></library></jukebox></data>')" 0 "HTTP/1.1 201 Created"
expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data -d '<data xmlns="urn:ietf:params:xml:ns:yang:ietf-restconf"><jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Foo Fighters</name><album><name>One by One</name><year>2012</year></album></artist><artist><name>Nick Cave and the Bad Seeds</name><album><name>Tender Prey</name><year>1988</year></album></artist></library></jukebox></data>')" 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' '<jukebox xmlns="http://example.com/ns/example-jukebox"><library><artist><name>Foo Fighters</name><album><name>One by One</name><year>2012</year></album></artist><artist><name>Nick Cave and the Bad Seeds</name><album><name>Tender Prey</name><year>1988</year></album></artist></library></jukebox>'
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="<playlist xmlns=\"http://example.com/ns/example-jukebox\"><name>Foo-One</name><song><index>0</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]</id></song><song><index>1</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]</id></song></playlist>"
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="<playlist xmlns=\"http://example.com/ns/example-jukebox\"><name>Foo-One</name><song><index>0</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]</id></song><song><index>2</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]</id></song><song><index>1</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]</id></song></playlist>"
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="<playlist xmlns=\"http://example.com/ns/example-jukebox\"><name>Foo-One</name><song><index>0</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]</id></song><song><index>2</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Bridge Burning'\]</id></song><song><index>3</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Something else'\]</id></song><song><index>1</index><id>/example-jukebox:jukebox/library/artist\[name='Foo Fighters'\]/album\[name='Wasting Light'\]/song\[name='Rope'\]</id></song></playlist>"
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' '<extra xmlns="http://example.com/ns/example-jukebox">2</extra><extra xmlns="http://example.com/ns/example-jukebox">3</extra><extra xmlns="http://example.com/ns/example-jukebox">1</extra>'
expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '<extra xmlns="http://example.com/ns/example-jukebox">2</extra><extra xmlns="http://example.com/ns/example-jukebox">3</extra><extra xmlns="http://example.com/ns/example-jukebox">1</extra>'
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' '<extra xmlns="http://example.com/ns/example-jukebox">2</extra><extra xmlns="http://example.com/ns/example-jukebox">4</extra><extra xmlns="http://example.com/ns/example-jukebox">3</extra><extra xmlns="http://example.com/ns/example-jukebox">1</extra>'
expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' '<extra xmlns="http://example.com/ns/example-jukebox">2</extra><extra xmlns="http://example.com/ns/example-jukebox">4</extra><extra xmlns="http://example.com/ns/example-jukebox">3</extra><extra xmlns="http://example.com/ns/example-jukebox">1</extra>'
if false; then # NYI