diff --git a/apps/restconf/clixon_restconf.h b/apps/restconf/clixon_restconf.h index 1499a4f3..4a1b7b04 100644 --- a/apps/restconf/clixon_restconf.h +++ b/apps/restconf/clixon_restconf.h @@ -42,8 +42,10 @@ * Types (also in restconf_lib.h) */ enum restconf_media{ - YANG_DATA_JSON, /* "application/yang-data+json" (default for RESTCONF) */ - YANG_DATA_XML /* "application/yang-data+xml" */ + YANG_DATA_JSON, /* "application/yang-data+json" */ + YANG_DATA_XML, /* "application/yang-data+xml" */ + YANG_PATCH_JSON, /* "application/yang-patch+json" */ + YANG_PATCH_XML /* "application/yang-patch+xml" */ }; typedef enum restconf_media restconf_media; diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index dd255f9f..ddde425e 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -32,6 +32,9 @@ ***** END LICENSE BLOCK ***** * @see https://nginx.org/en/docs/http/ngx_http_core_module.html#var_https + * @note The response payload for errors uses text_html. RFC7231 is vague + * on the response payload (and its media). Maybe it should be omitted + * altogether? */ #include @@ -76,8 +79,8 @@ static const map_str2int netconf_restconf_map[] = { {"bad-element", 400}, {"unknown-element", 400}, {"unknown-namespace", 400}, - {"access-denied", 401}, /* or 403 */ {"access-denied", 403}, + {"access-denied", 401}, /* or 403 */ {"lock-denied", 409}, {"resource-denied", 409}, {"rollback-failed", 500}, @@ -144,6 +147,8 @@ static const map_str2int http_reason_phrase_map[] = { static const map_str2int http_media_map[] = { {"application/yang-data+xml", YANG_DATA_XML}, {"application/yang-data+json", YANG_DATA_JSON}, + {"application/yang-patch+xml", YANG_PATCH_XML}, + {"application/yang-patch+json", YANG_PATCH_JSON}, {NULL, -1} }; @@ -172,6 +177,12 @@ restconf_media_int2str(restconf_media media) } /*! Return media_in from Content-Type, -1 if not found or unrecognized + * @note media-type syntax does not support parameters + * @see RFC7231 Sec 3.1.1.1 for media-type syntax type: + * media-type = type "/" subtype *( OWS ";" OWS parameter ) + * type = token + * subtype = token + * */ restconf_media restconf_content_type(FCGX_Request *r) @@ -514,6 +525,10 @@ api_return_err(clicon_handle h, FCGX_FPrintF(r->out, "%s", cbuf_get(cb)); FCGX_FPrintF(r->out, "}\r\n"); } + default: + clicon_err(OE_YANG, EINVAL, "Invalid media type %d", media); + goto done; + break; } /* switch media */ ok: retval = 0; diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 316a7ddc..6c4a2613 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -47,10 +47,13 @@ /*! RESTCONF media types * @see http_media_map + * (also in clixon_restconf.h) */ enum restconf_media{ - YANG_DATA_JSON, /* "application/yang-data+json" (default for RESTCONF) */ - YANG_DATA_XML /* "application/yang-data+xml" */ + YANG_DATA_JSON, /* "application/yang-data+json" */ + YANG_DATA_XML, /* "application/yang-data+xml" */ + YANG_PATCH_JSON, /* "application/yang-patch+json" */ + YANG_PATCH_XML /* "application/yang-patch+xml" */ }; typedef enum restconf_media restconf_media; diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index 1adf6cc2..31177590 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -249,6 +249,8 @@ api_root(clicon_handle h, if (xml2json_cbuf(cb, xt, pretty) < 0) goto done; break; + default: + break; } FCGX_FPrintF(r->out, "%s", cb?cbuf_get(cb):""); FCGX_FPrintF(r->out, "\r\n\r\n"); @@ -297,6 +299,8 @@ api_yang_library_version(clicon_handle h, if (xml2json_cbuf(cb, xt, pretty) < 0) goto done; break; + default: + break; } clicon_debug(1, "%s cb%s", __FUNCTION__, cbuf_get(cb)); FCGX_FPrintF(r->out, "%s\n", cb?cbuf_get(cb):""); diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index fd263954..b5437101 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -510,7 +510,7 @@ api_data_write(clicon_handle h, /* There is an api-path that defines an element in the datastore tree. * Not top-of-tree. */ - clicon_debug(1, "%s x:%s xbot:%s",__FUNCTION__, dname, xml_name(xbot)); + clicon_debug(1, "%s Comparing bottom-of api-path (%s) with top-of-data (%s)",__FUNCTION__, xml_name(xbot), dname); /* Check same symbol in api-path as data */ if (strcmp(dname, xml_name(xbot))){ @@ -683,7 +683,6 @@ api_data_write(clicon_handle h, FCGX_SetExitStatus(204, r->out); /* Replaced */ FCGX_FPrintF(r->out, "Status: 204 No Content\r\n"); } - FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); ok: retval = 0; @@ -794,13 +793,19 @@ api_data_patch(clicon_handle h, int ret; media_in = restconf_content_type(r); - if (media_in == YANG_DATA_XML || media_in == YANG_DATA_JSON){ - /* plain patch */ + switch (media_in){ + case YANG_DATA_XML: + case 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 */ + break; + case YANG_PATCH_XML: + case YANG_PATCH_JSON: /* RFC 8072 patch */ ret = restconf_notimplemented(r); + break; + default: + ret = restconf_unsupported_media(r); + break; } return ret; } diff --git a/apps/restconf/restconf_methods_get.c b/apps/restconf/restconf_methods_get.c index d8108185..1f7e8554 100644 --- a/apps/restconf/restconf_methods_get.c +++ b/apps/restconf/restconf_methods_get.c @@ -193,6 +193,8 @@ api_data_get2(clicon_handle h, if (xml2json_cbuf(cbx, xret, pretty) < 0) goto done; break; + default: + break; } } else{ @@ -244,6 +246,8 @@ api_data_get2(clicon_handle h, if (xml2json_cbuf_vec(cbx, xvec, xlen, pretty) < 0) goto done; break; + default: + break; } } clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx)); @@ -391,6 +395,8 @@ api_operations_get(clicon_handle h, case YANG_DATA_JSON: cprintf(cbx, "{\"operations\": {"); break; + default: + break; } ymod = NULL; i = 0; @@ -409,7 +415,10 @@ api_operations_get(clicon_handle h, cprintf(cbx, ","); cprintf(cbx, "\"%s:%s\": null", yang_argument_get(ymod), yang_argument_get(yc)); break; + default: + break; } + } } switch (media_out){ @@ -419,6 +428,8 @@ api_operations_get(clicon_handle h, case YANG_DATA_JSON: cprintf(cbx, "}}"); break; + default: + break; } FCGX_SetExitStatus(200, r->out); /* OK */ FCGX_FPrintF(r->out, "Content-Type: %s\r\n", restconf_media_int2str(media_out)); diff --git a/apps/restconf/restconf_methods_post.c b/apps/restconf/restconf_methods_post.c index 1c1f7c3c..55583eae 100644 --- a/apps/restconf/restconf_methods_post.c +++ b/apps/restconf/restconf_methods_post.c @@ -951,6 +951,8 @@ api_operations_post(clicon_handle h, goto done; /* xoutput should now look: {"example:output": {"x":0,"y":42}} */ break; + default: + break; } FCGX_FPrintF(r->out, "%s", cbuf_get(cbret)); FCGX_FPrintF(r->out, "\r\n\r\n"); diff --git a/lib/clixon/clixon_xml_nsctx.h b/lib/clixon/clixon_xml_nsctx.h index 98b7c2a3..e397c056 100644 --- a/lib/clixon/clixon_xml_nsctx.h +++ b/lib/clixon/clixon_xml_nsctx.h @@ -54,6 +54,6 @@ int xml_nsctx_set(cvec *nsc, char *prefix, char *namespace); cvec *xml_nsctx_init(char *prefix, char *namespace); int xml_nsctx_node(cxobj *x, cvec **ncp); int xml_nsctx_yang(yang_stmt *yn, cvec **ncp); -int xml_nsctx_free(cvec *ncs); +int xml_nsctx_free(cvec *nsc); #endif /* _CLIXON_XML_NSCTX_H */ diff --git a/lib/src/clixon_xml_nsctx.c b/lib/src/clixon_xml_nsctx.c index 7b5d62d8..db85456f 100644 --- a/lib/src/clixon_xml_nsctx.c +++ b/lib/src/clixon_xml_nsctx.c @@ -169,7 +169,7 @@ xml_nsctx_init(char *prefix, static int xml_nsctx_node1(cxobj *xn, - cvec *ncs) + cvec *nsc) { int retval = -1; cxobj *xa = NULL; @@ -186,30 +186,30 @@ xml_nsctx_node1(cxobj *xn, nm = xml_name(xa); if (pf == NULL){ if (strcmp(nm, "xmlns")==0 && /* set default namespace context */ - xml_nsctx_get(ncs, NULL) == NULL){ + xml_nsctx_get(nsc, NULL) == NULL){ val = xml_value(xa); - if (xml_nsctx_set(ncs, NULL, val) < 0) + if (xml_nsctx_set(nsc, NULL, val) < 0) goto done; } } else if (strcmp(pf, "xmlns")==0 && /* set prefixed namespace context */ - xml_nsctx_get(ncs, nm) == NULL){ + xml_nsctx_get(nsc, nm) == NULL){ val = xml_value(xa); - if (xml_nsctx_set(ncs, nm, val) < 0) + if (xml_nsctx_set(nsc, nm, val) < 0) goto done; } } if ((xp = xml_parent(xn)) == NULL){ #ifdef USE_NETCONF_NS_AS_DEFAULT /* If not default namespace defined, use the base netconf ns as default */ - if (xml_nsctx_get(ncs, NULL) == NULL) - if (xml_nsctx_set(ncs, NULL, NETCONF_BASE_NAMESPACE) < 0) + if (xml_nsctx_get(nsc, NULL) == NULL) + if (xml_nsctx_set(nsc, NULL, NETCONF_BASE_NAMESPACE) < 0) goto done; #endif } else - if (xml_nsctx_node1(xp, ncs) < 0) + if (xml_nsctx_node1(xp, nsc) < 0) goto done; retval = 0; done: @@ -345,9 +345,9 @@ xml_nsctx_yang(yang_stmt *yn, * @retval NULL Error */ int -xml_nsctx_free(cvec *ncs) +xml_nsctx_free(cvec *nsc) { - cvec *cvv = (cvec*)ncs; + cvec *cvv = (cvec*)nsc; if (cvv) cvec_free(cvv); diff --git a/test/test_nacm_module_write.sh b/test/test_nacm_module_write.sh index 53ee2056..7dca30ef 100755 --- a/test/test_nacm_module_write.sh +++ b/test/test_nacm_module_write.sh @@ -45,7 +45,6 @@ cat < $cfg false /usr/local/lib/$APPNAME/backend /usr/local/var/$APPNAME/$APPNAME.pidfile - 1 /usr/local/var/$APPNAME false internal diff --git a/test/test_restconf.sh b/test/test_restconf.sh index a50ab9c0..38ca60a6 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -101,7 +101,7 @@ expecteq "$(curl -s -H 'Accept: application/yang-data+json' -G http://localhost/ ' new "restconf options. RFC 8040 4.1" -expectpart "$(curl -is -X OPTIONS http://localhost/restconf/data)" 0 "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" +expectpart "$(curl -is -X OPTIONS http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" # -I means HEAD new "restconf HEAD. RFC 8040 4.2" diff --git a/test/test_restconf_patch.sh b/test/test_restconf_patch.sh index 932d8a58..746dee8f 100755 --- a/test/test_restconf_patch.sh +++ b/test/test_restconf_patch.sh @@ -1,5 +1,7 @@ #!/bin/bash # Restconf RFC8040 plain patch Sec 4.6 / 4.6.1 +# Use nacm module in example/main/example_restconf.c hardcoded to +# andy:bar and wilma:bar # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -7,6 +9,7 @@ s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi APPNAME=example cfg=$dir/conf.xml +startupdb=$dir/startup_db fjukebox=$dir/example-jukebox.yang cat < $cfg @@ -14,19 +17,82 @@ cat < $cfg $cfg /usr/local/share/clixon $IETFRFC - $fjukebox - false + $dir /usr/local/var/$APPNAME/$APPNAME.sock - /usr/local/lib/$APPNAME/backend + ietf-netconf:startup + false + /usr/local/lib/$APPNAME/restconf $dir/restconf.pidfile /usr/local/var/$APPNAME + $dir + internal EOF +cat< $startupdb + + + true + deny + deny + permit + + + admin + andy + + + limited + wilma + + + + admin + admin + + permit-all + * + * + permit + + Allow the 'admin' group complete access to all operations and data. + + + + + limited + limited + + limit-jukebox + jukebox-example + read create delete + deny + + + + +EOF + +# An extra testmodule that includes nacm +cat < $dir/example-system.yang + module example-system { + namespace "http://example.com/ns/example-system"; + prefix "ex"; + import ietf-netconf-acm { + prefix nacm; + } + container system { + leaf enable-jukebox-streaming { + type boolean; + } + } + } +EOF + # Common Jukebox spec (fjukebox must be set) . ./jukebox.sh -new "test params: -f $cfg" +new "test params: -s startup -f $cfg" if [ $BE -ne 0 ]; then new "kill old backend" @@ -35,15 +101,15 @@ if [ $BE -ne 0 ]; then err fi sudo pkill clixon_backend # to be sure - new "start backend -s init -f $cfg" - start_backend -s init -f $cfg + new "start backend -s startup -f $cfg" + start_backend -s startup -f $cfg fi new "kill old restconf daemon" sudo pkill -u www-data -f "/www-data/clixon_restconf" -new "start restconf daemon" -start_restconf -f $cfg +new "start restconf daemon (-a is enable basic authentication)" +start_restconf -f $cfg -- -a new "waiting" wait_backend @@ -51,20 +117,88 @@ wait_restconf # also in test_restconf.sh new "MUST support the PATCH method for a plain patch" -expectpart "$(curl -is -X OPTIONS http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" "Accept-Patch: application/yang-data+xml,application/yang-data+json" +expectpart "$(curl -u andy:bar -is -X OPTIONS http://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Allow: OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE" "Accept-Patch: application/yang-data+xml,application/yang-data+json" new "If the target resource instance does not exist, the server MUST NOT create it." -expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 400 Bad Request" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 400 Bad Request" new "Create it with PUT instead" -expectpart "$(curl -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 201 Created" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":null}')" 0 "HTTP/1.1 201 Created" new "THEN change it with PATCH" -expectpart "$(curl -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":{"library":{"artist":{"name":"Clash"}}}}')" 0 "HTTP/1.1 204 No Content" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox -d '{"example-jukebox:jukebox":{"library":{"artist":{"name":"Clash"}}}}')" 0 "HTTP/1.1 204 No Content" + +new "Check content (json)" +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Clash"}\]}}}' + +new "Check content (xml)" +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'Clash' + +new 'If the user is not authorized, "403 Forbidden" SHOULD be returned.' +expectpart "$(curl -u wilma:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 403 Forbidden" '{"ietf-restconf:errors":{"error":{"error-type":"application","error-tag":"access-denied","error-severity":"error","error-message":"default deny"}}}' + +new 'user is authorized' +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash -d '{"example-jukebox:artist":{"name":"Clash","album":{"name":"London Calling"}}}')" 0 "HTTP/1.1 204 No Content" + +# 4.6.1. Plain Patch + +new "restconf DELETE whole datastore" +expectpart "$(curl -u andy:bar -is -X DELETE http://localhost/restconf/data)" 0 "HTTP/1.1 204 No Content" + +new "Create album London Calling with PUT" +expectpart "$(curl -u andy:bar -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"}}')" 0 "HTTP/1.1 201 Created" + +new "The message-body for a plain patch MUST be present" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Beatles -d '')" 0 "HTTP/1.1 400 Bad Request" + +# Plain patch can be used to create or update, but not delete, a child +# resource within the target resource. +new "Create a child resource (genre and year)" +expectpart "$(curl -u andy:bar -si -X PATCH -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","genre":"example-jukebox:rock","year":"2129"}}')" 0 'HTTP/1.1 204 No Content' + +new "Update a child resource (year)" +expectpart "$(curl -u andy:bar -si -X PATCH -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 204 No Content' + +new "Check content xml" +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'London Callingrock1979' + +new "Check content json" +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:album":\[{"name":"London Calling","genre":"rock","year":1979}\]}' + +new "The message-body MUST be represented by the media type application/yang-data+xml (or +json ^)" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 204 No Content" + +new "Check content (xml)" +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+xml')" 0 'HTTP/1.1 200 OK' 'ClashLondon Callingjazz1979' + +new "not implemented media type" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-patch+xml' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 501 Not Implemented" + +new "wrong media type" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: text/html' http://localhost/restconf/data/example-jukebox:jukebox/library/artist=Clash/album=London%20Calling -d 'London Callingjazz')" 0 "HTTP/1.1 415 Unsupported Media Type" + +# If the target resource represents a YANG leaf-list, then the PATCH +# method MUST NOT change the value of the leaf-list instance. +# leaf-list extra{ +new "Create leaf-list a" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"a"}')" 0 "HTTP/1.1 201 Created" + +new "Create leaf-list b" +expectpart "$(curl -u andy:bar -si -X PUT -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=b -d '{"example-jukebox:extra":"b"}')" 0 "HTTP/1.1 201 Created" new "Check content" -expectpart "$(curl -si -X GET http://localhost/restconf/data/example-jukebox:jukebox -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:jukebox":{"library":{"artist":\[{"name":"Clash"}\]}}}' +expectpart "$(curl -u andy:bar -si -X GET http://localhost/restconf/data/example-jukebox:extra -H 'Accept: application/yang-data+json')" 0 'HTTP/1.1 200 OK' '{"example-jukebox:extra":\["a","b"\]}' +new "MUST NOT change the value of the leaf-list instance" +expectpart "$(curl -u andy:bar -si -X PATCH -H 'Content-Type: application/yang-data+json' http://localhost/restconf/data/example-jukebox:extra=a -d '{"example-jukebox:extra":"b"}')" 0 'HTTP/1.1 412 Precondition Failed' + +# If the target resource represents a YANG list instance, then the key +# leaf values, in message-body representation, MUST be the same as the +# key leaf values in the request URI. The PATCH method MUST NOT be +# used to change the key leaf values for a data resource instance. + +new "The key leaf values MUST be the same as the key leaf values in the request" +expectpart "$(curl -u andy:bar -si -X PATCH -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":"The Clash"}}')" 0 'HTTP/1.1 412 Precondition Failed' new "Kill restconf daemon" stop_restconf