First working prototype

This commit is contained in:
Olof hagsand 2020-10-27 09:20:30 +01:00
parent 78f5a6983c
commit acc6a0fec7
7 changed files with 353 additions and 4 deletions

View file

@ -49,6 +49,10 @@ Expected: September, 2021
* Restconf YANG PATCH according to RFC 8072 (Work in progress)
* Experimental: enable by setting YANG_PATCH in include/clixon_custom.h
* Thanks to Alan Yaniger for providing this patch
* List pagination
* This is prototype work for ietf netconf work
* See draft-wwlh-netconf-list-pagination-00.txt
* New http media: application/yang.collection+xml/json
### API changes on existing protocol/config features
@ -476,7 +480,7 @@ Developers may need to change their code
* This only applies to the evhtp restconf daemon, not fcgi/nginx, where the nginx config is used.
* The RESTCONF clixon-config options are obsolete
* Thanks to Dave Cornejo for the idea
### API changes on existing protocol/config features
Users may have to change how they access the system

View file

@ -534,6 +534,227 @@ api_data_collection(clicon_handle h,
return retval;
}
/*! GET Collection
* According to restconf collection draft. Lists, work in progress
* @param[in] h Clixon handle
* @param[in] req Generic Www 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 path starts
* @param[in] qvec Vector of query string (QUERY_STRING)
* @param[in] pretty Set to 1 for pretty-printed xml/json output
* @param[in] media_out Output media
* @param[in] head If 1 is HEAD, otherwise GET
* @code
* curl -X GET http://localhost/restconf/data/interfaces
* @endcode
* A collection resource contains a set of data resources. It is used
* to represent a all instances or a subset of all instances in a YANG
* list or leaf-list.
* @see draft-ietf-netconf-restconf-collection-00.txt
*/
static int
api_data_collection(clicon_handle h,
void *req,
char *api_path,
cvec *pcvec, /* XXX remove? */
int pi,
cvec *qvec,
int pretty,
restconf_media media_out)
{
int retval = -1;
char *xpath = NULL;
cbuf *cbx = NULL;
yang_stmt *yspec;
cxobj *xret = NULL;
cxobj *xerr = NULL; /* malloced */
cxobj *xe = NULL; /* not malloced */
cxobj **xvec = NULL;
int i;
int ret;
cvec *nsc = NULL;
char *attr; /* attribute value string */
netconf_content content = CONTENT_ALL;
cxobj *xtop = NULL;
cxobj *xbot = NULL;
yang_stmt *y = NULL;
cbuf *cbrpc = NULL;
char *depth;
char *count;
char *skip;
char *direction;
char *sort;
char *where;
clicon_debug(1, "%s", __FUNCTION__);
if ((yspec = clicon_dbspec_yang(h)) == NULL){
clicon_err(OE_FATAL, 0, "No DB_SPEC");
goto done;
}
/* strip /... from start */
for (i=0; i<pi; i++)
api_path = index(api_path+1, '/');
if (api_path){
if ((xtop = xml_new("top", NULL, CX_ELMNT)) == NULL)
goto done;
/* Translate api-path to xml, but to validate the api-path, note: strict=1
* xtop and xbot unnecessary for this function but needed by function
* Set strict=0 to accept list uri:s with =keys syntax
*/
if ((ret = api_path2xml(api_path, yspec, xtop, YC_DATANODE, 0, &xbot, &y, &xerr)) < 0)
goto done;
/* Translate api-path to xpath: xpath (cbpath) and namespace context (nsc)
* XXX: xpath not used in collection?
*/
if (ret != 0 &&
(ret = api_path2xpath(api_path, yspec, &xpath, &nsc, &xerr)) < 0)
goto done;
if (ret == 0){ /* validation failed */
if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){
clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)");
goto done;
}
if (api_return_err(h, req, xe, pretty, media_out, 0) < 0)
goto done;
goto ok;
}
if (yang_keyword_get(y) != Y_LIST && yang_keyword_get(y) != Y_LEAF_LIST){
if (netconf_bad_element_xml(&xerr, "application",
yang_argument_get(y),
"Element is not list or leaf-list which is required for GET collection") < 0)
goto done;
if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){
clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)");
goto done;
}
if (api_return_err(h, req, xe, pretty, media_out, 0) < 0)
goto done;
goto ok;
}
}
/* Check for content attribute */
if ((attr = cvec_find_str(qvec, "content")) != NULL){
clicon_debug(1, "%s content=%s", __FUNCTION__, attr);
if ((int)(content = netconf_content_str2int(attr)) == -1){
if (netconf_bad_attribute_xml(&xerr, "application",
"content", "Unrecognized value of content attribute") < 0)
goto done;
if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){
clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)");
goto done;
}
if (api_return_err(h, req, xe, pretty, media_out, 0) < 0)
goto done;
goto ok;
}
}
clicon_debug(1, "%s path:%s", __FUNCTION__, xpath);
if (content != CONTENT_CONFIG && content != CONTENT_NONCONFIG && content != CONTENT_ALL){
clicon_err(OE_XML, EINVAL, "Invalid content attribute %d", content);
goto done;
}
/* Clixon extensions and collection attributes */
depth = cvec_find_str(qvec, "depth");
count = cvec_find_str(qvec, "count");
skip = cvec_find_str(qvec, "skip");
direction = cvec_find_str(qvec, "direction");
sort = cvec_find_str(qvec, "sort");
where = cvec_find_str(qvec, "where");
if (clicon_rpc_get_collection(h, api_path, y, nsc, content,
depth, count, skip, direction, sort, where,
&xret) < 0){
if (netconf_operation_failed_xml(&xerr, "protocol", clicon_err_reason) < 0)
goto done;
if ((xe = xpath_first(xerr, NULL, "rpc-error")) == NULL){
clicon_err(OE_XML, EINVAL, "rpc-error not found (internal error)");
goto done;
}
if (api_return_err(h, req, xe, pretty, media_out, 0) < 0)
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.
*/
#if 0 /* DEBUG */
if (clicon_debug_get())
clicon_log_xml(LOG_DEBUG, xret, "%s xret:", __FUNCTION__);
#endif
/* Check if error return */
if ((xe = xpath_first(xret, NULL, "//rpc-error")) != NULL){
if (api_return_err(h, req, xe, pretty, media_out, 0) < 0)
goto done;
goto ok;
}
/* Normal return, no error */
if ((cbx = cbuf_new()) == NULL)
goto done;
switch (media_out){
case YANG_COLLECTION_XML:
if (clicon_xml2cbuf(cbx, xret, 0, pretty, -1) < 0) /* Dont print top object? */
goto done;
break;
case YANG_COLLECTION_JSON:
if (xml2json_cbuf(cbx, xret, pretty) < 0)
goto done;
break;
default:
if (restconf_unsupported_media(req) < 0)
goto done;
goto ok;
break;
}
#if 0
/* Check if not exists */
if (xlen == 0){
/* 4.3: If a retrieval request for a data resource represents an
instance that does not exist, then an error response containing
a "404 Not Found" status-line MUST be returned by the server.
The error-tag value "invalid-value" is used in this case. */
if (netconf_invalid_value_xml(&xerr, "application", "Instance does not exist") < 0)
goto done;
/* override invalid-value default 400 with 404 */
if ((xe = xpath_first(xerr, NULL, "rpc-error")) != NULL){
if (api_return_err(h, req, xe, pretty, media_out, 404) < 0)
goto done;
}
goto ok;
}
#endif
clicon_debug(1, "%s cbuf:%s", __FUNCTION__, cbuf_get(cbx));
if (restconf_reply_header(req, "Content-Type", "%s", restconf_media_int2str(media_out)) < 0)
goto done;
if (restconf_reply_header(req, "Cache-Control", "no-cache") < 0)
goto done;
if (restconf_reply_send(req, 200, cbx) < 0)
goto done;
ok:
retval = 0;
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
if (cbrpc)
cbuf_free(cbrpc);
if (xpath)
free(xpath);
if (nsc)
xml_nsctx_free(nsc);
if (xtop)
xml_free(xtop);
if (cbx)
cbuf_free(cbx);
if (xret)
xml_free(xret);
if (xerr)
xml_free(xerr);
if (xvec)
free(xvec);
return retval;
}
/*! REST HEAD method
* @param[in] h Clixon handle
* @param[in] req Generic Www handle

View file

@ -56,6 +56,11 @@
/* Collections namespace from draft-ietf-netconf-restconf-collection-00.txt
*/
#define NETCONF_COLLECTION_NAMESPACE "urn:ietf:params:xml:ns:yang:ietf-netconf-collection"
/* See RFC 7950 Sec 5.3.1: YANG defines an XML namespace for NETCONF <edit-config>
* operations, <error-info> content, and the <action> element.
*/
#define NETCONF_COLLECTION_NAMESPACE "urn:ietf:params:xml:ns:yang:ietf-netconf-list-pagination"
/* Output symbol for netconf get/get-config

View file

@ -1516,7 +1516,6 @@ netconf_module_load(clicon_handle h)
/* Load restconf yang. Note this is also a part of clixon-config */
if (yang_spec_parse_module(h, "clixon-restconf", NULL, yspec)< 0)
goto done;
#if 1
/* XXX: Both the following settings are because clicon-handle is not part of all API
* functions
* Treat unknown XML as anydata */
@ -1529,7 +1528,9 @@ netconf_module_load(clicon_handle h)
/* Load restconf collection */
if (yang_spec_parse_module(h, "ietf-netconf-list-pagination", NULL, yspec)< 0)
goto done;
/* Load restconf collection */
if (yang_spec_parse_module(h, "ietf-netconf-list-pagination", NULL, yspec)< 0)
goto done;
retval = 0;
done:
return retval;

114
test/test_collection.sh Executable file
View file

@ -0,0 +1,114 @@
#!/usr/bin/env bash
# Restconf RFC8040 Appendix A and B "jukebox" example
# For collection / scaling activity
# Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
APPNAME=example
cfg=$dir/conf.xml
fjukebox=$dir/example-jukebox.yang
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_FEATURE>ietf-netconf:startup</CLICON_FEATURE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<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>
<CLICON_BACKEND_PIDFILE>$dir/restconf.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>$dir</CLICON_XMLDB_DIR>
<CLICON_STREAM_DISCOVERY_RFC8040>true</CLICON_STREAM_DISCOVERY_RFC8040>
</clixon-config>
EOF
cat <<EOF > $dir/startup_db
<config>
<jukebox xmlns="http://example.com/ns/example-jukebox">
<library>
<artist>
<name>Foo Fighters</name>
<album xmlns="http://example.com/ns/example-jukebox">
<name>Crime and Punishment</name>
<year>1995</year>
</album>
<album xmlns="http://example.com/ns/example-jukebox">
<name>One by One</name>
<year>2002</year>
</album>
<album xmlns="http://example.com/ns/example-jukebox">
<name>The Color and the Shape</name>
<year>1997</year>
</album>
<album xmlns="http://example.com/ns/example-jukebox">
<name>There is Nothing Left to Loose</name>
<year>1999</year>
</album>
<album xmlns="http://example.com/ns/example-jukebox">
<name>White and Black</name>
<year>1998</year>
</album>
</artist>
</library>
</jukebox>
</config>
EOF
# Common Jukebox spec (fjukebox must be set)
. ./jukebox.sh
new "test params: -f $cfg -- -s" # XXX: -sS state file
if [ $BE -ne 0 ]; then
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
sudo pkill -f clixon_backend # to be sure
new "start backend -s startup -f $cfg"
start_backend -s startup -f "$cfg"
fi
new "waiting"
wait_backend
if [ $RC -ne 0 ]; then
new "kill old restconf daemon"
stop_restconf_pre
new "start restconf daemon"
start_restconf -f $cfg
new "waiting"
wait_restconf
fi
new "C.1. 'count' Parameter RESTCONF"
expectpart "$(curl $CURLOPTS -X GET -H "Accept: application/yang.collection+xml" $RCPROTO://localhost/restconf/data/example-jukebox:jukebox/library/artist=Foo%20Fighters/album/?count=2)" 0 "HTTP/1.1 200 OK" "application/yang.collection+xml" '<collection xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-collection"><album xmlns="http://example.com/ns/example-jukebox"><name>Crime and Punishment</name><year>1995</year></album><album xmlns="http://example.com/ns/example-jukebox"><name>One by One</name><year>2002</year></album></collection>'
new "C.1. 'count' Parameter NETCONF"
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc netconf:message-id=\"101\" xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\"><get-collection xmlns=\"urn:ietf:params:xml:ns:yang:ietf-netconf-collection\"><datastore>running</datastore><module-name>example-jukebox</module-name><list-target>/example-jukebox:jukebox/library/artist=Foo Fighters/album</list-target><count>2</count></get-collection></rpc>]]>]]>" '^<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" netconf:message-id="101"><collection xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-collection"><album xmlns="http://example.com/ns/example-jukebox"><name>Crime and Punishment</name><year>1995</year></album><album xmlns="http://example.com/ns/example-jukebox"><name>One by One</name><year>2002</year></album></collection></rpc-reply>]]>]]>$'
if [ $RC -ne 0 ]; then
new "Kill restconf daemon"
stop_restconf
fi
if [ $BE -eq 0 ]; then
exit # BE
fi
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
rm -rf $dir

View file

@ -300,6 +300,10 @@ function testrun()
new "restconf empty rpc JSON"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+json" -d {\"clixon-example:input\":null} $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/$HVER 204"
# -I / --head get headers only HEAD
new "restconf HEAD. RFC 8040 4.2"
expectpart "$(curl $CURLOPTS -I --head "Accept: application/yang-data+json" $RCPROTO://localhost/restconf/data)" 0 "HTTP/1.1 200 OK" "Content-Type: application/yang-data+json"
new "restconf empty rpc XML"
expectpart "$(curl $CURLOPTS -X POST -H "Content-Type: application/yang-data+xml" -d '<input xmlns="urn:example:clixon"></input>' $proto://$addr/restconf/operations/clixon-example:empty)" 0 "HTTP/$HVER 204"

View file

@ -342,6 +342,6 @@ if [ -z "$match" ]; then
fi
rm -rf $dir
new "endtest"
endtest