Restconf: get well-known, top-level resource, yang library version, put whole datastore,

check for different keys in put lists.
This commit is contained in:
Olof hagsand 2018-01-21 14:31:53 +01:00
parent 1ee3f7e67e
commit 26667b2c2f
11 changed files with 759 additions and 91 deletions

View file

@ -5,13 +5,15 @@
### Major changes:
### Minor changes:
* The following backward compatible options to configure have been obsoleted. If you havent already migrated this code you must do this now.
* Backend startup modes prior to 3.3.3. As enabled with `configure --with-startup-compat`. Configure option CLICON_USE_STARTUP_CONFIG is also obsoleted.
* Configuration files (non-XML) prior to 3.3.3. As enabled with `configure --with-config-compat`. The template clicon.conf.cpp files are also removed.
* 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. Thanks Stephen Jones for getting this right.
* Changed restconf GET to return object referenced. ie, GET /restconf/data/X returns X. Thanks Stephen Jones for getting this right.
* Restconf: get well-known, top-level resource, yang library version, put whole datastore, check for different keys in put lists.
* Default configure file added by Matt Smith. Config file is selected in the following priority order:
* Provide -f option when starting a program.

View file

@ -2,16 +2,15 @@
### Features
Clixon restconf is a daemon based on FASTCGI. Instructions are available to
run with NGINX.
The implementatation supports plain OPTIONS, HEAD, GET, POST, PUT, PATCH, DELETE.
and is based on draft-ietf-netconf-restconf-13.
There is currently (2017) a [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040), many of those features are _not_ implemented,
including:
run with NGINX.
The implementatation is based on [RFC 8040: RESTCONF Protocol](https://tools.ietf.org/html/rfc8040).
The following featires are supported:
- OPTIONS, HEAD, GET, POST, PUT, DELETE
The following are not implemented
- PATCH
- query parameters (section 4.9)
- notifications (sec 6)
- GET /restconf/ (sec 3.3)
- GET /restconf/yang-library-version (sec 3.3.3)
- only rudimentary error reporting exists (sec 7)
- schema resource
### Installation using Nginx

View file

@ -73,9 +73,14 @@
/* Command line options to be passed to getopt(3) */
#define RESTCONF_OPTS "hDf:p:y:"
/* Should be discovered via "/.well-known/host-meta"
resource ([RFC6415]) */
#define RESTCONF_API_ROOT "/restconf/"
/* RESTCONF enables deployments to specify where the RESTCONF API is
located. The client discovers this by getting the "/.well-known/host-meta"
resource
*/
#define RESTCONF_WELL_KNOWN "/.well-known/host-meta"
#define RESTCONF_API "restconf"
#define RESTCONF_API_ROOT "/restconf"
/*! Generic REST method, GET, PUT, DELETE, etc
* @param[in] h CLIXON handle
@ -117,6 +122,7 @@ api_data(clicon_handle h,
retval = api_data_delete(h, r, api_path, pi);
else
retval = notfound(r);
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
return retval;
}
@ -144,18 +150,118 @@ api_operations(clicon_handle h,
clicon_debug(1, "%s", __FUNCTION__);
request_method = FCGX_GetParam("REQUEST_METHOD", r->envp);
clicon_debug(1, "%s method:%s", __FUNCTION__, request_method);
if (strcmp(request_method, "POST")==0)
if (strcmp(request_method, "GET")==0)
retval = api_operation_get(h, r, path, pcvec, pi, qvec, data);
else if (strcmp(request_method, "POST")==0)
retval = api_operation_post(h, r, path, pcvec, pi, qvec, data);
else
retval = notfound(r);
return retval;
}
/*! Retrieve the Top-Level API Resource
* @note Only returns null for operations and data,...
*/
static int
api_root(clicon_handle h,
FCGX_Request *r)
{
int retval = -1;
char *media_accept;
int use_xml = 0; /* By default use JSON */
cxobj *xt = NULL;
cbuf *cb = NULL;
int pretty;
clicon_debug(1, "%s", __FUNCTION__);
pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY");
media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp);
if (strcmp(media_accept, "application/yang-data+xml")==0)
use_xml++;
FCGX_SetExitStatus(200, r->out); /* OK */
FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json");
FCGX_FPrintF(r->out, "\r\n");
if (xml_parse_string("<restconf><data></data><operations></operations><yang-library-version>2016-06-21</yang-library-version></restconf>", NULL, &xt) < 0)
goto done;
if ((cb = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
if (xml_rootchild(xt, 0, &xt) < 0)
goto done;
if (use_xml){
if (clicon_xml2cbuf(cb, xt, 0, pretty) < 0)
goto done;
}
else
if (xml2json_cbuf(cb, xt, pretty) < 0)
goto done;
FCGX_FPrintF(r->out, "%s", cb?cbuf_get(cb):"");
FCGX_FPrintF(r->out, "\r\n\r\n");
retval = 0;
done:
if (cb)
cbuf_free(cb);
if (xt)
xml_free(xt);
return retval;
}
/*!
* See https://tools.ietf.org/html/rfc7895
*/
static int
api_yang_library_version(clicon_handle h,
FCGX_Request *r)
{
int retval = -1;
char *media_accept;
int use_xml = 0; /* By default use JSON */
cxobj *xt = NULL;
cbuf *cb = NULL;
int pretty;
clicon_debug(1, "%s", __FUNCTION__);
pretty = clicon_option_bool(h, "CLICON_RESTCONF_PRETTY");
media_accept = FCGX_GetParam("HTTP_ACCEPT", r->envp);
if (strcmp(media_accept, "application/yang-data+xml")==0)
use_xml++;
FCGX_SetExitStatus(200, r->out); /* OK */
FCGX_FPrintF(r->out, "Content-Type: application/yang-data+%s\r\n", use_xml?"xml":"json");
FCGX_FPrintF(r->out, "\r\n");
if (xml_parse_string("<yang-library-version>2016-06-21</yang-library-version>", NULL, &xt) < 0)
goto done;
if (xml_rootchild(xt, 0, &xt) < 0)
goto done;
if ((cb = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
if (use_xml){
if (clicon_xml2cbuf(cb, xt, 0, pretty) < 0)
goto done;
}
else{
if (xml2json_cbuf(cb, xt, pretty) < 0)
goto done;
}
clicon_debug(1, "%s cb%s", __FUNCTION__, cbuf_get(cb));
FCGX_FPrintF(r->out, "%s\r\n", cb?cbuf_get(cb):"");
FCGX_FPrintF(r->out, "\r\n\r\n");
retval = 0;
done:
if (cb)
cbuf_free(cb);
if (xt)
xml_free(xt);
return retval;
}
/*! Process a FastCGI request
* @param[in] r Fastcgi request handle
*/
static int
request_process(clicon_handle h,
api_restconf(clicon_handle h,
FCGX_Request *r)
{
int retval = -1;
@ -176,7 +282,28 @@ request_process(clicon_handle h,
query = FCGX_GetParam("QUERY_STRING", r->envp);
if ((pvec = clicon_strsep(path, "/", &pn)) == NULL)
goto done;
/* Sanity check of path. Should be /restconf/ */
if (pn < 2){
retval = notfound(r);
goto done;
}
if (strlen(pvec[0]) != 0){
retval = notfound(r);
goto done;
}
if (strcmp(pvec[1], RESTCONF_API)){
retval = notfound(r);
goto done;
}
if (pn == 2){
retval = api_root(h, r);
goto done;
}
if ((method = pvec[2]) == NULL){
retval = notfound(r);
goto done;
}
clicon_debug(1, "method=%s", method);
if (str2cvec(query, '&', '=', &qvec) < 0)
goto done;
if (str2cvec(path, '/', '=', &pcvec) < 0) /* rest url eg /album=ricky/foo */
@ -188,10 +315,7 @@ request_process(clicon_handle h,
clicon_debug(1, "DATA=%s", data);
if (str2cvec(data, '&', '=', &dvec) < 0)
goto done;
if ((method = pvec[2]) == NULL){
retval = notfound(r);
goto done;
}
retval = 0;
test(r, 1);
/* If present, check credentials */
@ -202,8 +326,9 @@ request_process(clicon_handle h,
if (auth == 0)
goto done;
clicon_debug(1, "%s credentials ok 2", __FUNCTION__);
if (strcmp(method, "data") == 0) /* restconf, skip /api/data */
if (strcmp(method, "yang-library-version")==0)
retval = api_yang_library_version(h, r);
else if (strcmp(method, "data") == 0) /* restconf, skip /api/data */
retval = api_data(h, r, path, pcvec, 2, qvec, data);
else if (strcmp(method, "operations") == 0) /* rpc */
retval = api_operations(h, r, path, pcvec, 2, qvec, data);
@ -226,6 +351,24 @@ request_process(clicon_handle h,
return retval;
}
/*! Process a FastCGI request
* @param[in] r Fastcgi request handle
*/
static int
api_well_known(clicon_handle h,
FCGX_Request *r)
{
clicon_debug(1, "%s", __FUNCTION__);
FCGX_FPrintF(r->out, "Content-Type: application/xrd+xml\r\n");
FCGX_FPrintF(r->out, "\r\n");
FCGX_SetExitStatus(200, r->out); /* OK */
FCGX_FPrintF(r->out, "<XRD xmlns='http://docs.oasis-open.org/ns/xri/xrd-1.0'>\r\n");
FCGX_FPrintF(r->out, " <Link rel='restconf' href='/restconf'/>\r\n");
FCGX_FPrintF(r->out, "</XRD>\r\n");
return 0;
}
static int
restconf_terminate(clicon_handle h)
{
@ -384,13 +527,17 @@ main(int argc,
}
clicon_debug(1, "------------");
if ((path = FCGX_GetParam("REQUEST_URI", r->envp)) != NULL){
if (strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)) == 0 ||
strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)-1) == 0)
request_process(h, r); /* This is the function */
clicon_debug(1, "path:%s", path);
if (strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)) == 0)
api_restconf(h, r); /* This is the function */
else if (strncmp(path, RESTCONF_WELL_KNOWN, strlen(RESTCONF_WELL_KNOWN)) == 0) {
api_well_known(h, r); /* This is the function */
}
else{
clicon_debug(1, "top-level not found");
clicon_debug(1, "top-level %s not found", path);
notfound(r);
}
}
else
clicon_debug(1, "NULL URI");

View file

@ -379,7 +379,7 @@ api_data_get(clicon_handle h,
return api_data_get2(h, r, pcvec, pi, qvec, 0);
}
/*! REST POST method
/*! Generic REST POST 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)
@ -387,7 +387,7 @@ api_data_get(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
* @note We map post to edit-config create.
* @note restconf POST is mapped to edit-config create.
POST:
target resource type is datastore --> create a top-level resource
target resource type is data resource --> create child resource
@ -414,12 +414,12 @@ api_data_post(clicon_handle h,
cvec *qvec,
char *data)
{
enum operation_type op = OP_CREATE;
int retval = -1;
enum operation_type op = OP_CREATE;
int i;
cxobj *xdata = NULL;
cxobj *xtop = NULL; /* xpath root */
cbuf *cbx = NULL;
cxobj *xtop = NULL; /* xpath root */
cxobj *xbot = NULL;
cxobj *x;
yang_node *y = NULL;
@ -444,8 +444,8 @@ api_data_post(clicon_handle h,
/* Create config top-of-tree */
if ((xtop = xml_new("config", NULL, NULL)) == NULL)
goto done;
/* Translate api_path to xtop/xbot */
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 */
@ -456,29 +456,35 @@ api_data_post(clicon_handle h,
}
}
else if (json_parse_str(data, &xdata) < 0){
badrequest(r);
goto ok;
badrequest(r);
goto ok;
}
/* Add xdata to xbot */
x = NULL;
while ((x = xml_child_each(xdata, x, CX_ELMNT)) != NULL) {
if ((xa = xml_new("operation", x, NULL)) == NULL)
goto done;
xml_type_set(xa, CX_ATTR);
if (xml_value_set(xa, xml_operation2str(op)) < 0)
goto done;
if (xml_addsub(xbot, x) < 0)
goto done;
/* The message-body MUST contain exactly one instance of the
* expected data resource.
*/
if (xml_child_nr(xdata) != 1){
badrequest(r);
goto ok;
}
x = xml_child_i(xdata,0);
/* Add operation (create/replace) as attribute */
if ((xa = xml_new("operation", x, NULL)) == NULL)
goto done;
xml_type_set(xa, CX_ATTR);
if (xml_value_set(xa, xml_operation2str(op)) < 0)
goto done;
/* Replace xbot with x, ie bottom of api-path with data */
if (xml_addsub(xbot, x) < 0)
goto done;
/* Create text buffer for transfer to backend */
if ((cbx = cbuf_new()) == NULL)
goto done;
if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0)
goto done;
clicon_debug(1, "%s xml: %s",__FUNCTION__, cbuf_get(cbx));
clicon_debug(1, "%s xml: %s api_path:%s",__FUNCTION__, cbuf_get(cbx), api_path);
if (clicon_rpc_edit_config(h, "candidate",
OP_NONE,
cbuf_get(cbx)) < 0){
// notfound(r); /* XXX */
conflict(r);
goto ok;
}
@ -492,7 +498,6 @@ api_data_post(clicon_handle h,
goto done;
FCGX_SetExitStatus(201, r->out); /* Created */
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;
@ -505,6 +510,58 @@ api_data_post(clicon_handle h,
if (cbx)
cbuf_free(cbx);
return retval;
} /* api_data_post */
/*! Check matching keys
*
* @param[in] y Yang statement, should be list or leaf-list
* @param[in] xdata XML data tree
* @param[in] xapipath XML api-path tree
* @retval 0 Yes, keys match
* @retval -1 No keys do not match
* If the target resource represents a YANG leaf-list, then the PUT
* method MUST NOT change the value of the leaf-list instance.
*
* 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 PUT method MUST NOT be used
* to change the key leaf values for a data resource instance.
*/
static int
match_list_keys(yang_stmt *y,
cxobj *xdata,
cxobj *xapipath)
{
int retval = -1;
cvec *cvk = NULL; /* vector of index keys */
cg_var *cvi;
char *keyname;
cxobj *xkeya; /* xml key object in api-path */
cxobj *xkeyd; /* xml key object in data */
char *keya;
char *keyd;
if (y->ys_keyword != Y_LIST &&y->ys_keyword != Y_LEAF_LIST)
return -1;
cvk = y->ys_cvec; /* Use Y_LIST cache, see ys_populate_list() */
cvi = NULL;
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
keyname = cv_string_get(cvi);
if ((xkeya = xml_find(xapipath, keyname)) == NULL)
goto done; /* No key in api-path */
keya = xml_body(xkeya);
if ((xkeyd = xml_find(xdata, keyname)) == NULL)
goto done; /* No key in data */
keyd = xml_body(xkeyd);
if (strcmp(keya, keyd) != 0)
goto done; /* keys dont match */
}
retval = 0;
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
return retval;
}
/*! Generic REST PUT method
@ -515,6 +572,7 @@ api_data_post(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
* @note restconf PUT is mapped to edit-config replace.
* @example
curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1
*
@ -528,7 +586,7 @@ api_data_post(clicon_handle h,
int
api_data_put(clicon_handle h,
FCGX_Request *r,
char *api_path,
char *api_path0,
cvec *pcvec,
int pi,
cvec *qvec,
@ -539,19 +597,19 @@ api_data_put(clicon_handle h,
int i;
cxobj *xdata = NULL;
cbuf *cbx = NULL;
cxobj *x;
cxobj *xtop = NULL; /* xpath root */
cxobj *xbot = NULL;
cxobj *xtop = NULL;
cxobj *xp;
cxobj *xparent;
cxobj *x;
yang_node *y = NULL;
yang_spec *yspec;
cxobj *xa;
char *media_content_type;
int parse_xml = 0; /* By default expect and parse JSON */
char *api_path;
clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"",
__FUNCTION__,
api_path, data);
__FUNCTION__, api_path0, data);
media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp);
if (media_content_type &&
strcmp(media_content_type, "application/yang-data+xml")==0)
@ -560,11 +618,13 @@ api_data_put(clicon_handle h,
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 xtop/xbot */
xbot = xtop;
if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0)
goto done;
@ -579,22 +639,48 @@ api_data_put(clicon_handle h,
badrequest(r);
goto ok;
}
/* The message-body MUST contain exactly one instance of the
* expected data resource.
*/
if (xml_child_nr(xdata) != 1){
badrequest(r);
goto ok;
}
x = xml_child_i(xdata,0);
/* Add operation (create/replace) as attribute */
if ((xa = xml_new("operation", x, NULL)) == NULL)
goto done;
xml_type_set(xa, CX_ATTR);
if (xml_value_set(xa, xml_operation2str(op)) < 0)
goto done;
/* XXX Special case path=/restconf/data xml_name(x) == data */
/* Replace xbot with x */
xp = xml_parent(xbot);
xml_purge(xbot);
if (xml_addsub(xp, x) < 0)
goto done;
#if 1 /* This is different from POST */
/* Replace xparent with x, ie bottom of api-path with data */
if (api_path==NULL && strcmp(xml_name(x),"data")==0){
if (xml_addsub(NULL, x) < 0)
goto done;
xtop = x;
xml_name_set(xtop, "config");
}
else {
/* Check same symbol in api-path as data */
if (strcmp(xml_name(x), xml_name(xbot))){
badrequest(r);
goto ok;
}
/* If list or leaf-list, api-path keys must match data keys */
if (y && (y->yn_keyword == Y_LIST ||y->yn_keyword == Y_LEAF_LIST)){
if (match_list_keys((yang_stmt*)y, x, xbot) < 0){
badrequest(r);
goto ok;
}
}
xparent = xml_parent(xbot);
xml_purge(xbot);
if (xml_addsub(xparent, x) < 0)
goto done;
}
#endif
/* Create text buffer for transfer to backend */
if ((cbx = cbuf_new()) == NULL)
goto done;
if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0)
@ -628,8 +714,7 @@ api_data_put(clicon_handle h,
if (cbx)
cbuf_free(cbx);
return retval;
}
} /* api_data_put */
/*! Generic REST PATCH method
* @param[in] h CLIXON handle
@ -724,6 +809,20 @@ api_data_delete(clicon_handle h,
return retval;
}
/*! NYI
*/
int
api_operation_get(clicon_handle h,
FCGX_Request *r,
char *path,
cvec *pcvec,
int pi,
cvec *qvec,
char *data)
{
return 0;
}
/*! REST operation POST method
* @param[in] h CLIXON handle
* @param[in] r Fastcgi request handle

View file

@ -60,6 +60,10 @@ int api_data_patch(clicon_handle h, FCGX_Request *r, char *api_path,
cvec *qvec, char *data);
int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi);
int api_operation_get(clicon_handle h, FCGX_Request *r,
char *path,
cvec *pcvec, int pi, cvec *qvec, char *data);
int api_operation_post(clicon_handle h, FCGX_Request *r,
char *path,
cvec *pcvec, int pi, cvec *qvec, char *data);

View file

@ -748,14 +748,19 @@ text_modify_top(cxobj *x0,
cxobj *x0c; /* base child */
cxobj *x1c; /* mod child */
yang_stmt *yc; /* yang child */
char *opstr;
/* Assure top-levels are 'config' */
assert(x0 && strcmp(xml_name(x0),"config")==0);
assert(x1 && strcmp(xml_name(x1),"config")==0);
/* Check for operations embedded in tree according to netconf */
if ((opstr = xml_find_value(x1, "operation")) != NULL)
if (xml_operation(opstr, &op) < 0)
goto done;
/* Special case if x1 is empty, top-level only <config/> */
if (!xml_child_nr(x1)){ /* base tree not empty */
if (xml_child_nr(x0))
if (!xml_child_nr(x1)){
if (xml_child_nr(x0)) /* base tree not empty */
switch(op){
case OP_DELETE:
case OP_REMOVE:

View file

@ -1654,9 +1654,9 @@ api_path2xml(char *api_path,
if (nvec > 1 && !strlen(vec[nvec-1]))
nvec--;
if (nvec < 1){
clicon_err(OE_XML, 0, "Malformed key: %s", api_path);
goto done;
}
clicon_err(OE_XML, 0, "Malformed key: %s", api_path);
goto done;
}
nvec--; /* NULL-terminated */
if (api_path2xml_vec(vec+1, nvec,
xpath, (yang_node*)yspec, schemanode,

View file

@ -2,7 +2,7 @@
# Transactions per second for large lists read/write plotter using gnuplot
#
. ./lib.sh
max=1000 # Nr of db entries
max=200 # Nr of db entries
step=100
reqs=1000
cfg=$dir/scaling-conf.xml
@ -48,27 +48,55 @@ EOF
run(){
nr=$1 # Number of entries in DB
reqs=$2
write=$3
mode=$3
echo -n "<rpc><edit-config><target><candidate/></target><config><x>" > $fconfig
echo -n "<rpc><edit-config><target><candidate/></target><default-operation>replace</default-operation><config><x>" > $fconfig
for (( i=0; i<$nr; i++ )); do
echo -n "<c>$i</c>" >> $fconfig
echo -n "<y><a>$i</a><b>$i</b></y>" >> $fconfig
done
echo "</x></config></edit-config></rpc>]]>]]>" >> $fconfig
expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
if $write; then
time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><edit-config><target><candidate/></target><config><x><y><a>$rnd</a><b>$rnd</b></y></x></config></edit-config></rpc>]]>]]>"
done | $clixon_netconf -qf $cfg -y $fyang > /dev/null
else # read
case $mode in
readlist)
time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><get-config><source><candidate/></source><filter type=\"xpath\" select=\"/x/y[a=$rnd][b=$rnd]\" /></get-config></rpc>]]>]]>"
done | $clixon_netconf -qf $cfg -y $fyang > /dev/null
fi
;;
writelist)
time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><edit-config><target><candidate/></target><config><x><y><a>$rnd</a><b>$rnd</b></y></x></config></edit-config></rpc>]]>]]>"
done | $clixon_netconf -qf $cfg -y $fyang > /dev/null
;;
readleaflist)
time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><get-config><source><candidate/></source><filter type=\"xpath\" select=\"/x[c=$rnd]\" /></get-config></rpc>]]>]]>"
done | $clixon_netconf -qf $cfg -y $fyang > /dev/null
;;
writeleaflist)
time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><edit-config><target><candidate/></target><config><x><c>$rnd</c></x></config></edit-config></rpc>]]>]]>"
done | $clixon_netconf -qf $cfg -y $fyang > /dev/null
;;
esac
expecteof "$clixon_netconf -qf $cfg" "<rpc><discard-changes/></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
}
step(){
i=$1
mode=$2
echo -n "" > $fconfig
t=$(TEST=%e run $i $reqs $mode $ 2>&1 | awk '/real/ {print $2}')
# t is time in secs of $reqs -> transactions per second. $reqs
p=$(echo "$reqs/$t" | bc -lq)
# p is transactions per second.
echo "$i $p" >> $dir/$mode
}
once()(
@ -84,18 +112,19 @@ once()(
err
fi
# Always as a start
for (( i=10; i<=$step; i=i+10 )); do
step $i readlist
step $i writelist
step $i readleaflist
step $i writeleaflist
done
# Actual steps
for (( i=$step; i<=$max; i=i+$step )); do
t=$(TEST=%e run $i $reqs true $ 2>&1 | awk '/real/ {print $2}')
# t is time in secs of $reqs -> transactions per second. $reqs
p=$(echo "$reqs/$t" | bc -lq)
# p is transactions per second.
echo "$i $p" >> $dir/write
t=$(TEST=%e run $i $reqs false $ 2>&1 | awk '/real/ {print $2}')
# t is time in secs of $reqs -> transactions per second. $reqs
p=$(echo "$reqs/$t" | bc -lq)
# p is transactions per second.
echo "$i $p" >> $dir/read
step $i readlist
step $i readleaflist
step $i writelist
step $i writeleaflist
done
# Check if still alive
@ -118,7 +147,7 @@ set title "Clixon transactions per second r/w large lists" font ",14" textcolor
set xlabel "entries"
set ylabel "transactions per second"
set terminal wxt enhanced title "CLixon transactions " persist raise
plot "$dir/read" with linespoints title "read", "$dir/write" with linespoints title "write"
plot "$dir/readlist" with linespoints title "read list", "$dir/writelist" with linespoints title "write list", "$dir/readleaflist" with linespoints title "read leaf-list", "$dir/writeleaflist" with linespoints title "write leaf-list"
EOF
rm -rf $dir

View file

@ -88,15 +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" "<Link rel='restconf' href='/restconf'/>"
new "restconf get restconf json"
expectfn "curl -sSG http://localhost/restconf" '{"data": null,"operations": null,"yang-library-version": "2016-06-21"}}'
new "restconf get restconf/yang-library-version json"
expectfn "curl -sSG http://localhost/restconf/yang-library-version" '{"yang-library-version": "2016-06-21"}'
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}"

141
test/test_restconf2.sh Executable file
View file

@ -0,0 +1,141 @@
#!/bin/bash
# Restconf basic functionality
# Assume http server setup, such as nginx described in apps/restconf/README.md
# include err() and new() functions and creates $dir
. ./lib.sh
cfg=$dir/conf.xml
fyang=$dir/restconf.yang
# <CLICON_YANG_MODULE_MAIN>example</CLICON_YANG_MODULE_MAIN>
cat <<EOF > $cfg
<config>
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/var</CLICON_YANG_DIR>
<CLICON_YANG_MODULE_MAIN>$fyang</CLICON_YANG_MODULE_MAIN>
<CLICON_RESTCONF_PRETTY>false</CLICON_RESTCONF_PRETTY>
<CLICON_SOCK>/usr/local/var/routing/routing.sock</CLICON_SOCK>
<CLICON_BACKEND_PIDFILE>/usr/local/var/routing/routing.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_CLI_GENMODEL_COMPLETION>1</CLICON_CLI_GENMODEL_COMPLETION>
<CLICON_XMLDB_DIR>/usr/local/var/routing</CLICON_XMLDB_DIR>
<CLICON_XMLDB_PLUGIN>/usr/local/lib/xmldb/text.so</CLICON_XMLDB_PLUGIN>
</config>
EOF
cat <<EOF > $fyang
module example{
container interfaces-config{
list interface{
key name;
leaf name{
type string;
}
leaf type{
type string;
}
leaf description{
type string;
}
leaf netgate-if-type{
type string;
}
leaf enabled{
type boolean;
}
}
}
}
EOF
# kill old backend (if any)
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
new "start backend -s init -f $cfg -y $fyang"
sudo clixon_backend -s init -f $cfg -y $fyang
if [ $? -ne 0 ]; then
err
fi
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
sleep 1
new "restconf tests"
new "restconf PUT change key error"
#expectfn 'curl -s -X PUT -d {"interface":{"name":"ALPHA","type":"eth0"}} http://localhost/restconf/data/interfaces-config/interface=TEST' "fail"
#exit
new "restconf POST initial tree"
expectfn 'curl -s -X POST -d {"interfaces-config":{"interface":{"name":"local0","netgate-if-type":"regular"}}} http://localhost/restconf/data' ""
new "restconf GET datastore"
expectfn "curl -s -X GET http://localhost/restconf/data" '{"data": {"interfaces-config": {"interface": {"name": "local0","netgate-if-type": "regular"}}}}'
new "restconf GET interface"
expectfn "curl -s -X GET http://localhost/restconf/data/interfaces-config/interface=local0" '{"interface": {"name": "local0","netgate-if-type": "regular"}}'
new "restconf GET if-type"
expectfn "curl -s -X GET http://localhost/restconf/data/interfaces-config/interface=local0/netgate-if-type" '{"netgate-if-type": "regular"}'
new "restconf POST interface"
expectfn 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/interfaces-config' ""
new "restconf POST again"
expectfn 'curl -s -X POST -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/interfaces-config' "Data resource already exis"
new "restconf POST from top"
expectfn 'curl -s -X POST -d {"interfaces-config":{"interface":{"name":"TEST","type":"eth0"}}} http://localhost/restconf/data' "Data resource already exists"
new "restconf DELETE"
expectfn 'curl -s -X DELETE http://localhost/restconf/data/interfaces-config' ""
new "restconf GET null datastore"
expectfn "curl -s -X GET http://localhost/restconf/data" '{"data": null}'
new "restconf POST initial tree"
expectfn 'curl -s -X POST -d {"interfaces-config":{"interface":{"name":"local0","netgate-if-type":"regular"}}} http://localhost/restconf/data' ""
new "restconf PUT initial datastore"
expectfn 'curl -s -X PUT -d {"data":{"interfaces-config":{"interface":{"name":"local0","netgate-if-type":"regular"}}}} http://localhost/restconf/data' ""
new "restconf GET datastore"
expectfn "curl -s -X GET http://localhost/restconf/data" '{"data": {"interfaces-config": {"interface": {"name": "local0","netgate-if-type": "regular"}}}}'
new "restconf PUT change interface"
expectfn 'curl -s -X PUT -d {"interface":{"name":"local0","type":"atm0"}} http://localhost/restconf/data/interfaces-config/interface=local0' ""
new "restconf GET datastore"
expectfn "curl -s -X GET http://localhost/restconf/data" '{"data": {"interfaces-config": {"interface": {"name": "local0","type": "atm0"}}}}'
new "restconf PUT add interface"
expectfn 'curl -s -X PUT -d {"interface":{"name":"TEST","type":"eth0"}} http://localhost/restconf/data/interfaces-config/interface=TEST' ""
new "restconf PUT change key error"
expectfn 'curl -is -X PUT -d {"interface":{"name":"ALPHA","type":"eth0"}} http://localhost/restconf/data/interfaces-config/interface=TEST' "Bad request"
new "Kill restconf daemon"
sudo pkill -u www-data clixon_restconf
new "Kill backend"
# Check if still alive
pid=`pgrep clixon_backend`
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err "kill backend"
fi
rm -rf $dir

View file

@ -0,0 +1,242 @@
module ietf-yang-library {
namespace "urn:ietf:params:xml:ns:yang:ietf-yang-library";
prefix "yanglib";
import ietf-yang-types {
prefix yang;
}
import ietf-inet-types {
prefix inet;
}
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: <https://datatracker.ietf.org/wg/netconf/>
WG List: <mailto:netconf@ietf.org>
WG Chair: Mehmet Ersue
<mailto:mehmet.ersue@nsn.com>
WG Chair: Mahesh Jethanandani
<mailto:mjethanandani@gmail.com>
Editor: Andy Bierman
<mailto:andy@yumaworks.com>
Editor: Martin Bjorklund
<mailto:mbj@tail-f.com>
Editor: Kent Watsen
<mailto:kwatsen@juniper.net>";
description
"This module contains monitoring information about the YANG
modules and submodules that are used within a YANG-based
server.
Copyright (c) 2016 IETF Trust and the persons identified as
authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with or
without modification, is permitted pursuant to, and subject
to the license terms contained in, the Simplified BSD License
set forth in Section 4.c of the IETF Trust's Legal Provisions
Relating to IETF Documents
(http://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC 7895; see
the RFC itself for full legal notices.";
revision 2016-06-21 {
description
"Initial revision.";
reference
"RFC 7895: YANG Module Library.";
}
/*
* Typedefs
*/
typedef revision-identifier {
type string {
pattern '\d{4}-\d{2}-\d{2}';
}
description
"Represents a specific date in YYYY-MM-DD format.";
}
/*
* Groupings
*/
grouping module-list {
description
"The module data structure is represented as a grouping
so it can be reused in configuration or another monitoring
data structure.";
grouping common-leafs {
description
"Common parameters for YANG modules and submodules.";
leaf name {
type yang:yang-identifier;
description
"The YANG module or submodule name.";
}
leaf revision {
type union {
type revision-identifier;
type string { length 0; }
}
description
"The YANG module or submodule revision date.
A zero-length string is used if no revision statement
is present in the YANG module or submodule.";
}
}
grouping schema-leaf {
description
"Common schema leaf parameter for modules and submodules.";
leaf schema {
type inet:uri;
description
"Contains a URL that represents the YANG schema
resource for this module or submodule.
This leaf will only be present if there is a URL
available for retrieval of the schema for this entry.";
}
}
list module {
key "name revision";
description
"Each entry represents one revision of one module
currently supported by the server.";
uses common-leafs;
uses schema-leaf;
leaf namespace {
type inet:uri;
mandatory true;
description
"The XML namespace identifier for this module.";
}
leaf-list feature {
type yang:yang-identifier;
description
"List of YANG feature names from this module that are
supported by the server, regardless of whether they are
defined in the module or any included submodule.";
}
list deviation {
key "name revision";
description
"List of YANG deviation module names and revisions
used by this server to modify the conformance of
the module associated with this entry. Note that
the same module can be used for deviations for
multiple modules, so the same entry MAY appear
within multiple 'module' entries.
The deviation module MUST be present in the 'module'
list, with the same name and revision values.
The 'conformance-type' value will be 'implement' for
the deviation module.";
uses common-leafs;
}
leaf conformance-type {
type enumeration {
enum implement {
description
"Indicates that the server implements one or more
protocol-accessible objects defined in the YANG module
identified in this entry. This includes deviation
statements defined in the module.
For YANG version 1.1 modules, there is at most one
module entry with conformance type 'implement' for a
particular module name, since YANG 1.1 requires that,
at most, one revision of a module is implemented.
For YANG version 1 modules, there SHOULD NOT be more
than one module entry for a particular module name.";
}
enum import {
description
"Indicates that the server imports reusable definitions
from the specified revision of the module but does
not implement any protocol-accessible objects from
this revision.
Multiple module entries for the same module name MAY
exist. This can occur if multiple modules import the
same module but specify different revision dates in
the import statements.";
}
}
mandatory true;
description
"Indicates the type of conformance the server is claiming
for the YANG module identified by this entry.";
}
list submodule {
key "name revision";
description
"Each entry represents one submodule within the
parent module.";
uses common-leafs;
uses schema-leaf;
}
}
}
/*
* Operational state data nodes
*/
container modules-state {
config false;
description
"Contains YANG module monitoring information.";
leaf module-set-id {
type string;
mandatory true;
description
"Contains a server-specific identifier representing
the current set of modules and submodules. The
server MUST change the value of this leaf if the
information represented by the 'module' list instances
has changed.";
}
uses module-list;
}
/*
* Notifications
*/
notification yang-library-change {
description
"Generated when the set of modules and submodules supported
by the server has changed.";
leaf module-set-id {
type leafref {
path "/yanglib:modules-state/yanglib:module-set-id";
}
mandatory true;
description
"Contains the module-set-id value representing the
set of modules and submodules supported at the server at
the time the notification is generated.";
}
}
}