diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d7a1eaa..59f45c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,13 @@ * Clixon XML C-lib prior to 3.4.0. As enabled with `configure --with-xml-compat` * new configuration option: CLICON_RESTCONF_PRETTY -* Changed RESTCONF GET to return object referenced. ie, GET /restconf/data/X returns X. +* Changed RESTCONF GET to return object referenced. ie, GET /restconf/data/X returns X. Thanks Stephen Jones for getting this right. +* Default configure file added by Matt Smith. Config file is selected in the following priority order: + * Provide -f option when starting a program. + * Provide --with-configfile=FILE when configuring + * /etc/clixon.xml + ### Corrected Bugs * Corrected "No yang spec" printed on tty on leafref CLI usage diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index f7aa7c0b..f796d8c3 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -160,7 +160,7 @@ api_data_get_err(clicon_handle h, goto done; if ((xtag = xpath_first(xerr, "/error-tag")) == NULL){ notfound(r); /* bad reply? */ - goto done; + goto ok; } code = restconf_err2code(xml_body(xtag)); if ((reason_phrase = restconf_code2reason(code)) == NULL) @@ -180,6 +180,7 @@ api_data_get_err(clicon_handle h, FCGX_FPrintF(r->out, " %s", cbuf_get(cbj)); FCGX_FPrintF(r->out, " }\r\n"); FCGX_FPrintF(r->out, "}\r\n"); + ok: retval = 0; done: if (cbj) @@ -247,13 +248,13 @@ api_data_get2(clicon_handle h, /* We know "data" is element pi-1 */ if (api_path2xpath_cvv(yspec, pcvec, pi, cbpath) < 0){ notfound(r); - goto done; + goto ok; } path = cbuf_get(cbpath); clicon_debug(1, "%s path:%s", __FUNCTION__, path); if (clicon_rpc_get(h, path, &xret) < 0){ notfound(r); - goto done; + goto ok; } /* We get return via netconf which is complete tree from root * We need to cut that tree to only the object. @@ -431,7 +432,8 @@ api_data_post(clicon_handle h, __FUNCTION__, api_path, data); media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp); - if (strcmp(media_content_type, "application/yang-data+xml")==0) + if (media_content_type && + strcmp(media_content_type, "application/yang-data+xml")==0) parse_xml++; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); @@ -443,18 +445,19 @@ api_data_post(clicon_handle h, if ((xtop = xml_new("config", NULL, NULL)) == NULL) goto done; xbot = xtop; + /* xbot is resulting xml tree on exit */ if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0) goto done; /* Parse input data as json or xml into xml */ if (parse_xml){ if (xml_parse_string(data, NULL, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } } else if (json_parse_str(data, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } /* Add xdata to xbot */ x = NULL; @@ -477,14 +480,13 @@ api_data_post(clicon_handle h, cbuf_get(cbx)) < 0){ // notfound(r); /* XXX */ conflict(r); - goto done; + goto ok; } if (clicon_rpc_validate(h, "candidate") < 0){ if (clicon_rpc_discard_changes(h) < 0) goto done; badrequest(r); - retval = 0; - goto done; + goto ok; } if (clicon_rpc_commit(h) < 0) goto done; @@ -492,6 +494,7 @@ api_data_post(clicon_handle h, FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); // XXX api_path can be null FCGX_FPrintF(r->out, "Location: %s\r\n", api_path); FCGX_FPrintF(r->out, "\r\n"); + ok: retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); @@ -550,7 +553,8 @@ api_data_put(clicon_handle h, __FUNCTION__, api_path, data); media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp); - if (strcmp(media_content_type, "application/yang-data+xml")==0) + if (media_content_type && + strcmp(media_content_type, "application/yang-data+xml")==0) parse_xml++; if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_FATAL, 0, "No DB_SPEC"); @@ -567,18 +571,17 @@ api_data_put(clicon_handle h, /* Parse input data as json or xml into xml */ if (parse_xml){ if (xml_parse_string(data, NULL, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } } else if (json_parse_str(data, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } if (xml_child_nr(xdata) != 1){ badrequest(r); - retval = 0; - goto done; + goto ok; } x = xml_child_i(xdata,0); if ((xa = xml_new("operation", x, NULL)) == NULL) @@ -600,21 +603,21 @@ api_data_put(clicon_handle h, OP_NONE, cbuf_get(cbx)) < 0){ notfound(r); - goto done; + goto ok; } if (clicon_rpc_validate(h, "candidate") < 0){ if (clicon_rpc_discard_changes(h) < 0) goto done; badrequest(r); - retval = 0; - goto done; + goto ok; } if (clicon_rpc_commit(h) < 0) goto done; FCGX_SetExitStatus(201, r->out); /* Created */ FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); + ok: retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); @@ -703,13 +706,14 @@ api_data_delete(clicon_handle h, OP_NONE, cbuf_get(cbx)) < 0){ notfound(r); - goto done; + goto ok; } if (clicon_rpc_commit(h) < 0) goto done; FCGX_SetExitStatus(201, r->out); FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); FCGX_FPrintF(r->out, "\r\n"); + ok: retval = 0; done: if (cbx) @@ -762,12 +766,15 @@ api_operation_post(clicon_handle h, int parse_xml = 0; /* By default expect and parse JSON */ char *media_accept; int use_xml = 0; /* By default return JSON */ + int pretty; clicon_debug(1, "%s json:\"%s\" path:\"%s\"", __FUNCTION__, data, path); + pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY"); if ((media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp)) && strcmp(media_accept, "application/yang-data+xml")==0) use_xml++; - if ((media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp)) && + media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp); + if (media_content_type && strcmp(media_content_type, "application/yang-data+xml")==0) parse_xml++; clicon_debug(1, "%s accept:\"%s\" content-type:\"%s\"", @@ -785,7 +792,7 @@ api_operation_post(clicon_handle h, goto done; if (yrpc == NULL){ retval = notfound(r); - goto done; + goto ok; } /* Create an xml message: * <"rpc">... @@ -801,13 +808,13 @@ api_operation_post(clicon_handle h, /* Parse input data as json or xml into xml */ if (parse_xml){ if (xml_parse_string(data, NULL, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } } else if (json_parse_str(data, &xdata) < 0){ - clicon_debug(1, "%s json parse fail: %s", __FUNCTION__, data); - goto done; + badrequest(r); + goto ok; } /* xdata should have format */ if ((xinput = xpath_first(xdata, "/input")) != NULL){ @@ -854,9 +861,10 @@ api_operation_post(clicon_handle h, if ((cbx = cbuf_new()) == NULL) goto done; xoutput=xpath_first(xret, "/"); + xml_name_set(xoutput, "output"); if ((youtput = yang_find((yang_node*)yrpc, Y_OUTPUT, NULL)) != NULL && xoutput){ - xml_name_set(xoutput, "output"); + clicon_debug(1, "%s xoutput:%s", __FUNCTION__, cbuf_get(cbx)); cbuf_reset(cbx); xml_spec_set(xoutput, youtput); /* needed for xml_spec_populate */ @@ -874,16 +882,17 @@ api_operation_post(clicon_handle h, FCGX_FPrintF(r->out, "\r\n"); if (xoutput){ if (use_xml){ - if (clicon_xml2cbuf(cbx, xoutput, 0, 1) < 0) + if (clicon_xml2cbuf(cbx, xoutput, 0, pretty) < 0) goto done; } else - if (xml2json_cbuf(cbx, xoutput, 1) < 0) + if (xml2json_cbuf(cbx, xoutput, pretty) < 0) goto done; clicon_debug(1, "%s xoutput:%s", __FUNCTION__, cbuf_get(cbx)); FCGX_FPrintF(r->out, "%s", cbx?cbuf_get(cbx):""); FCGX_FPrintF(r->out, "\r\n\r\n"); } + ok: retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); diff --git a/example/routing_backend.c b/example/routing_backend.c index 449706c5..59c7388b 100644 --- a/example/routing_backend.c +++ b/example/routing_backend.c @@ -134,6 +134,18 @@ fib_route(clicon_handle h, /* Clicon handle */ return 0; } +/*! Smallest possible RPC declaration for test */ +static int +empty(clicon_handle h, /* Clicon handle */ + cxobj *xe, /* Request: */ + struct client_entry *ce, /* Client session */ + cbuf *cbret, /* Reply eg ... */ + void *arg) /* Argument given at register */ +{ + cprintf(cbret, ""); + return 0; +} + /*! IETF Routing route-count rpc */ static int route_count(clicon_handle h, @@ -201,6 +213,11 @@ plugin_init(clicon_handle h) "route-count"/* Xml tag when callback is made */ ) < 0) goto done; + if (backend_rpc_cb_register(h, empty, + NULL, + "empty"/* Xml tag when callback is made */ + ) < 0) + goto done; retval = 0; done: return retval; diff --git a/lib/src/clixon_json.c b/lib/src/clixon_json.c index e261183c..50fd06a4 100644 --- a/lib/src/clixon_json.c +++ b/lib/src/clixon_json.c @@ -623,7 +623,7 @@ json_parse(char *str, * @param[in] str String containing JSON * @param[out] xt On success a top of XML parse tree is created with name 'top' * @retval 0 OK - * @retval -1 Error with clicon_err called + * @retval -1 Error with clicon_err called. Includes parse errors * * @code * cxobj *cx = NULL; diff --git a/lib/src/clixon_xml.c b/lib/src/clixon_xml.c index a6021c4b..c412bebf 100644 --- a/lib/src/clixon_xml.c +++ b/lib/src/clixon_xml.c @@ -1355,8 +1355,8 @@ xml_parse_file(int fd, * @param[in] str String containing XML definition. * @param[in] yspec Yang specification, or NULL * @param[in,out] xt Pointer to XML parse tree. If empty will be created. - * @retval 0 OK - * @retval -1 Error with clicon_err called + * @retval 0 OK + * @retval -1 Error with clicon_err called. Includes parse error * * @code * cxobj *xt = NULL; diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index b7ea80fc..bb4d6ade 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -1750,6 +1750,7 @@ xml_merge1(cxobj *x0, /*! Merge XML trees x1 into x0 according to yang spec yspec * @note both x0 and x1 need to be top-level trees * @see text_modify_top as more generic variant (in datastore text) + * @note returns -1 if YANG do not match, you may want to have a softer error */ int xml_merge(cxobj *x0, diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 395e2827..1593e633 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -7,11 +7,12 @@ cfg=$dir/conf.xml fyang=$dir/restconf.yang +# example cat < $cfg $cfg /usr/local/share/routing/yang - example + $fyang /usr/local/lib/routing/clispec /usr/local/lib/routing/backend /usr/local/lib/routing/restconf @@ -28,6 +29,7 @@ EOF cat < $fyang module example{ + prefix ex; import ietf-ip { prefix ip; } @@ -38,6 +40,16 @@ module example{ prefix "inet"; revision-date "2013-07-15"; } + rpc empty { + } + rpc input { + input { + } + } + rpc output { + output { + } + } } EOF @@ -60,7 +72,7 @@ new "kill old restconf daemon" sudo pkill -u www-data clixon_restconf new "start restconf daemon" -sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c www-data -- -Df $cfg # -D +sudo start-stop-daemon -S -q -o -b -x /www-data/clixon_restconf -d /www-data -c www-data -- -Df $cfg -D sleep 1 @@ -76,13 +88,15 @@ expectfn "curl -sS -I http://localhost/restconf/data" "HTTP/1.1 200 OK" new "restconf root discovery" expectfn "curl -sS -X GET http://localhost/.well-known/host-meta" "" +new "restconf empty rpc" +expectfn 'curl -sS -X POST -d {"input":{"name":""}} http://localhost/restconf/operations/ex:empty' '{"output": null}' + #new "restconf get restconf json XXX" #expectfn "curl -sSG http://localhost/restconf" "{\"restconf\" : $state }" #new "restconf get restconf/yang-library-version json XXX" #expectfn "curl -sSG http://localhost/restconf/yang-library-version" "{\"restconf\" : $state }" - new "restconf get empty config + state json" expectfn "curl -sSG http://localhost/restconf/data" "{\"data\": $state}" @@ -145,6 +159,9 @@ expectfn 'curl -sS -X POST -d {"interface":{"name":"eth/0/0","type":"eth","enabl new "Add leaf description using POST" expectfn 'curl -sS -X POST -d {"description":"The-first-interface"} http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "" +new "Add nothing using POST" +expectfn 'curl -sS -X POST http://localhost/restconf/data/interfaces/interface=eth%2f0%2f0' "data is in some way badly formed" + new "restconf Check description added" expectfn "curl -sS -G http://localhost/restconf/data" '{"interfaces": {"interface": {"name": "eth/0/0","description": "The-first-interface","type": "eth","enabled": "true"}} $' @@ -165,15 +182,13 @@ new "restconf get subtree" expectfn "curl -sS -G http://localhost/restconf/data" '{"interfaces": {"interface": {"name": "eth/0/0","type": "eth","enabled": "true"}},"interfaces-state": {"interface": {"name": "eth0","type": "eth","if-index": "42"}}} $' -new "restconf operation rpc using POST json" -expectfn 'curl -sS -X POST -d {"input":{"routing-instance-name":"ipv4"}} http://localhost/restconf/operations/rt:fib-route' '{ "output": { "route": { "address-family": "ipv4", "next-hop": { "next-hop-list": "2.3.4.5" } } } } ' - -exit # XXX +new "restconf rpc using POST json" +expectfn 'curl -sS -X POST -d {"input":{"routing-instance-name":"ipv4"}} http://localhost/restconf/operations/rt:fib-route' '{"output": {"route": {"address-family": "ipv4","next-hop": {"next-hop-list": "2.3.4.5"}}}}' new "restconf rpc using POST xml" # Cant get shell macros to work, inline matching from lib.sh ret=$(curl -sS -X POST -H "Accept: application/yang-data+xml" -d '{"input":{"routing-instance-name":"ipv4"}}' http://localhost/restconf/operations/rt:fib-route) -expect=" ipv4 2.3.4.5 " +expect="ipv42.3.4.5" match=`echo $ret | grep -EZo "$expect"` if [ -z "$match" ]; then err "$expect" "$ret"