First working prototype
This commit is contained in:
parent
78f5a6983c
commit
acc6a0fec7
7 changed files with 353 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
114
test/test_collection.sh
Executable 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
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -342,6 +342,6 @@ if [ -z "$match" ]; then
|
|||
fi
|
||||
|
||||
rm -rf $dir
|
||||
|
||||
new "endtest"
|
||||
endtest
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue