From 26667b2c2fe18d571908aa8b2449a80f1ecc26a9 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 21 Jan 2018 14:31:53 +0100 Subject: [PATCH] Restconf: get well-known, top-level resource, yang library version, put whole datastore, check for different keys in put lists. --- CHANGELOG.md | 4 +- apps/restconf/README.md | 15 +- apps/restconf/restconf_main.c | 179 ++++++++++++++++-- apps/restconf/restconf_methods.c | 167 +++++++++++++---- apps/restconf/restconf_methods.h | 4 + datastore/text/clixon_xmldb_text.c | 9 +- lib/src/clixon_xml_map.c | 6 +- test/plot_perf.sh | 71 +++++--- test/test_restconf.sh | 12 +- test/test_restconf2.sh | 141 ++++++++++++++ yang/ietf-yang-library@2016-06-21.yang | 242 +++++++++++++++++++++++++ 11 files changed, 759 insertions(+), 91 deletions(-) create mode 100755 test/test_restconf2.sh create mode 100644 yang/ietf-yang-library@2016-06-21.yang diff --git a/CHANGELOG.md b/CHANGELOG.md index 59f45c49..9e57114f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/apps/restconf/README.md b/apps/restconf/README.md index a7952a9c..fdb1da37 100644 --- a/apps/restconf/README.md +++ b/apps/restconf/README.md @@ -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 diff --git a/apps/restconf/restconf_main.c b/apps/restconf/restconf_main.c index f400edf0..b386616b 100644 --- a/apps/restconf/restconf_main.c +++ b/apps/restconf/restconf_main.c @@ -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("2016-06-21", 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("2016-06-21", 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, "\r\n"); + FCGX_FPrintF(r->out, " \r\n"); + FCGX_FPrintF(r->out, "\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"); diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index 48313a9f..859cf210 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -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; iyn_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 diff --git a/apps/restconf/restconf_methods.h b/apps/restconf/restconf_methods.h index 89ece997..0aef4428 100644 --- a/apps/restconf/restconf_methods.h +++ b/apps/restconf/restconf_methods.h @@ -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); diff --git a/datastore/text/clixon_xmldb_text.c b/datastore/text/clixon_xmldb_text.c index 9eae988e..739e2256 100644 --- a/datastore/text/clixon_xmldb_text.c +++ b/datastore/text/clixon_xmldb_text.c @@ -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 */ - 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: diff --git a/lib/src/clixon_xml_map.c b/lib/src/clixon_xml_map.c index bb4d6ade..bd7c1dbd 100644 --- a/lib/src/clixon_xml_map.c +++ b/lib/src/clixon_xml_map.c @@ -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, diff --git a/test/plot_perf.sh b/test/plot_perf.sh index 04b48f87..db73f7cc 100755 --- a/test/plot_perf.sh +++ b/test/plot_perf.sh @@ -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 "" > $fconfig + echo -n "replace" > $fconfig for (( i=0; i<$nr; i++ )); do + echo -n "$i" >> $fconfig echo -n "$i$i" >> $fconfig done echo "]]>]]>" >> $fconfig expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^]]>]]>$" - if $write; then - time -p for (( i=0; i<$reqs; i++ )); do - rnd=$(( ( RANDOM % $nr ) )) - echo "$rnd$rnd]]>]]>" -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 "]]>]]>" done | $clixon_netconf -qf $cfg -y $fyang > /dev/null - fi + ;; + writelist) + time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $nr ) )) + echo "$rnd$rnd]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang > /dev/null + ;; + readleaflist) + time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $nr ) )) + echo "]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang > /dev/null + ;; + writeleaflist) + time -p for (( i=0; i<$reqs; i++ )); do + rnd=$(( ( RANDOM % $nr ) )) + echo "$rnd]]>]]>" +done | $clixon_netconf -qf $cfg -y $fyang > /dev/null + ;; + esac + expecteof "$clixon_netconf -qf $cfg" "]]>]]>" "^]]>]]>$" +} + +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 diff --git a/test/test_restconf.sh b/test/test_restconf.sh index 1593e633..8cc700d6 100755 --- a/test/test_restconf.sh +++ b/test/test_restconf.sh @@ -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" "" +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}" diff --git a/test/test_restconf2.sh b/test/test_restconf2.sh new file mode 100755 index 00000000..1419f41b --- /dev/null +++ b/test/test_restconf2.sh @@ -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 + +# example +cat < $cfg + + $cfg + /usr/local/var + $fyang + + false + /usr/local/var/routing/routing.sock + /usr/local/var/routing/routing.pidfile + 1 + /usr/local/var/routing + /usr/local/lib/xmldb/text.so + +EOF + +cat < $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 diff --git a/yang/ietf-yang-library@2016-06-21.yang b/yang/ietf-yang-library@2016-06-21.yang new file mode 100644 index 00000000..1e897180 --- /dev/null +++ b/yang/ietf-yang-library@2016-06-21.yang @@ -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: + WG List: + + WG Chair: Mehmet Ersue + + + WG Chair: Mahesh Jethanandani + + + Editor: Andy Bierman + + + Editor: Martin Bjorklund + + + Editor: Kent Watsen + "; + + 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."; + } + } +} \ No newline at end of file