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: ### Major changes:
### Minor 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. * 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. * 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. * 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` * Clixon XML C-lib prior to 3.4.0. As enabled with `configure --with-xml-compat`
* new configuration option: CLICON_RESTCONF_PRETTY * 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: * Default configure file added by Matt Smith. Config file is selected in the following priority order:
* Provide -f option when starting a program. * Provide -f option when starting a program.

View file

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

View file

@ -73,9 +73,14 @@
/* Command line options to be passed to getopt(3) */ /* Command line options to be passed to getopt(3) */
#define RESTCONF_OPTS "hDf:p:y:" #define RESTCONF_OPTS "hDf:p:y:"
/* Should be discovered via "/.well-known/host-meta" /* RESTCONF enables deployments to specify where the RESTCONF API is
resource ([RFC6415]) */ located. The client discovers this by getting the "/.well-known/host-meta"
#define RESTCONF_API_ROOT "/restconf/" 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 /*! Generic REST method, GET, PUT, DELETE, etc
* @param[in] h CLIXON handle * @param[in] h CLIXON handle
@ -117,6 +122,7 @@ api_data(clicon_handle h,
retval = api_data_delete(h, r, api_path, pi); retval = api_data_delete(h, r, api_path, pi);
else else
retval = notfound(r); retval = notfound(r);
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
return retval; return retval;
} }
@ -144,18 +150,118 @@ api_operations(clicon_handle h,
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
request_method = FCGX_GetParam("REQUEST_METHOD", r->envp); request_method = FCGX_GetParam("REQUEST_METHOD", r->envp);
clicon_debug(1, "%s method:%s", __FUNCTION__, request_method); 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); retval = api_operation_post(h, r, path, pcvec, pi, qvec, data);
else else
retval = notfound(r); retval = notfound(r);
return retval; 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 /*! Process a FastCGI request
* @param[in] r Fastcgi request handle * @param[in] r Fastcgi request handle
*/ */
static int static int
request_process(clicon_handle h, api_restconf(clicon_handle h,
FCGX_Request *r) FCGX_Request *r)
{ {
int retval = -1; int retval = -1;
@ -176,7 +282,28 @@ request_process(clicon_handle h,
query = FCGX_GetParam("QUERY_STRING", r->envp); query = FCGX_GetParam("QUERY_STRING", r->envp);
if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) if ((pvec = clicon_strsep(path, "/", &pn)) == NULL)
goto done; 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) if (str2cvec(query, '&', '=', &qvec) < 0)
goto done; goto done;
if (str2cvec(path, '/', '=', &pcvec) < 0) /* rest url eg /album=ricky/foo */ 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); clicon_debug(1, "DATA=%s", data);
if (str2cvec(data, '&', '=', &dvec) < 0) if (str2cvec(data, '&', '=', &dvec) < 0)
goto done; goto done;
if ((method = pvec[2]) == NULL){
retval = notfound(r);
goto done;
}
retval = 0; retval = 0;
test(r, 1); test(r, 1);
/* If present, check credentials */ /* If present, check credentials */
@ -202,8 +326,9 @@ request_process(clicon_handle h,
if (auth == 0) if (auth == 0)
goto done; goto done;
clicon_debug(1, "%s credentials ok 2", __FUNCTION__); clicon_debug(1, "%s credentials ok 2", __FUNCTION__);
if (strcmp(method, "yang-library-version")==0)
if (strcmp(method, "data") == 0) /* restconf, skip /api/data */ 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); retval = api_data(h, r, path, pcvec, 2, qvec, data);
else if (strcmp(method, "operations") == 0) /* rpc */ else if (strcmp(method, "operations") == 0) /* rpc */
retval = api_operations(h, r, path, pcvec, 2, qvec, data); retval = api_operations(h, r, path, pcvec, 2, qvec, data);
@ -226,6 +351,24 @@ request_process(clicon_handle h,
return retval; 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 static int
restconf_terminate(clicon_handle h) restconf_terminate(clicon_handle h)
{ {
@ -384,13 +527,17 @@ main(int argc,
} }
clicon_debug(1, "------------"); clicon_debug(1, "------------");
if ((path = FCGX_GetParam("REQUEST_URI", r->envp)) != NULL){ if ((path = FCGX_GetParam("REQUEST_URI", r->envp)) != NULL){
if (strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)) == 0 || clicon_debug(1, "path:%s", path);
strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)-1) == 0) if (strncmp(path, RESTCONF_API_ROOT, strlen(RESTCONF_API_ROOT)) == 0)
request_process(h, r); /* This is the function */ 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{ else{
clicon_debug(1, "top-level not found"); clicon_debug(1, "top-level %s not found", path);
notfound(r); notfound(r);
} }
} }
else else
clicon_debug(1, "NULL URI"); 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); return api_data_get2(h, r, pcvec, pi, qvec, 0);
} }
/*! REST POST method /*! Generic REST POST method
* @param[in] h CLIXON handle * @param[in] h CLIXON handle
* @param[in] r Fastcgi request handle * @param[in] r Fastcgi request handle
* @param[in] api_path According to restconf (Sec 3.5.3.1 in rfc8040) * @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] pi Offset, where to start pcvec
* @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] qvec Vector of query string (QUERY_STRING)
* @param[in] data Stream input data * @param[in] data Stream input data
* @note We map post to edit-config create. * @note restconf POST is mapped to edit-config create.
POST: POST:
target resource type is datastore --> create a top-level resource target resource type is datastore --> create a top-level resource
target resource type is data resource --> create child resource target resource type is data resource --> create child resource
@ -414,12 +414,12 @@ api_data_post(clicon_handle h,
cvec *qvec, cvec *qvec,
char *data) char *data)
{ {
enum operation_type op = OP_CREATE;
int retval = -1; int retval = -1;
enum operation_type op = OP_CREATE;
int i; int i;
cxobj *xdata = NULL; cxobj *xdata = NULL;
cxobj *xtop = NULL; /* xpath root */
cbuf *cbx = NULL; cbuf *cbx = NULL;
cxobj *xtop = NULL; /* xpath root */
cxobj *xbot = NULL; cxobj *xbot = NULL;
cxobj *x; cxobj *x;
yang_node *y = NULL; yang_node *y = NULL;
@ -444,8 +444,8 @@ api_data_post(clicon_handle h,
/* Create config top-of-tree */ /* Create config top-of-tree */
if ((xtop = xml_new("config", NULL, NULL)) == NULL) if ((xtop = xml_new("config", NULL, NULL)) == NULL)
goto done; goto done;
/* Translate api_path to xtop/xbot */
xbot = xtop; xbot = xtop;
/* xbot is resulting xml tree on exit */
if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0) if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0)
goto done; goto done;
/* Parse input data as json or xml into xml */ /* 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){ else if (json_parse_str(data, &xdata) < 0){
badrequest(r); badrequest(r);
goto ok; goto ok;
} }
/* Add xdata to xbot */ /* The message-body MUST contain exactly one instance of the
x = NULL; * expected data resource.
while ((x = xml_child_each(xdata, x, CX_ELMNT)) != NULL) { */
if ((xa = xml_new("operation", x, NULL)) == NULL) if (xml_child_nr(xdata) != 1){
goto done; badrequest(r);
xml_type_set(xa, CX_ATTR); goto ok;
if (xml_value_set(xa, xml_operation2str(op)) < 0)
goto done;
if (xml_addsub(xbot, x) < 0)
goto done;
} }
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) if ((cbx = cbuf_new()) == NULL)
goto done; goto done;
if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0)
goto done; 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", if (clicon_rpc_edit_config(h, "candidate",
OP_NONE, OP_NONE,
cbuf_get(cbx)) < 0){ cbuf_get(cbx)) < 0){
// notfound(r); /* XXX */
conflict(r); conflict(r);
goto ok; goto ok;
} }
@ -492,7 +498,6 @@ api_data_post(clicon_handle h,
goto done; goto done;
FCGX_SetExitStatus(201, r->out); /* Created */ FCGX_SetExitStatus(201, r->out); /* Created */
FCGX_FPrintF(r->out, "Content-Type: text/plain\r\n"); 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"); FCGX_FPrintF(r->out, "\r\n");
ok: ok:
retval = 0; retval = 0;
@ -505,6 +510,58 @@ api_data_post(clicon_handle h,
if (cbx) if (cbx)
cbuf_free(cbx); cbuf_free(cbx);
return retval; 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 /*! Generic REST PUT method
@ -515,6 +572,7 @@ api_data_post(clicon_handle h,
* @param[in] pi Offset, where to start pcvec * @param[in] pi Offset, where to start pcvec
* @param[in] qvec Vector of query string (QUERY_STRING) * @param[in] qvec Vector of query string (QUERY_STRING)
* @param[in] data Stream input data * @param[in] data Stream input data
* @note restconf PUT is mapped to edit-config replace.
* @example * @example
curl -X PUT -d '{"enabled":"false"}' http://127.0.0.1/restconf/data/interfaces/interface=eth1 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 int
api_data_put(clicon_handle h, api_data_put(clicon_handle h,
FCGX_Request *r, FCGX_Request *r,
char *api_path, char *api_path0,
cvec *pcvec, cvec *pcvec,
int pi, int pi,
cvec *qvec, cvec *qvec,
@ -539,19 +597,19 @@ api_data_put(clicon_handle h,
int i; int i;
cxobj *xdata = NULL; cxobj *xdata = NULL;
cbuf *cbx = NULL; cbuf *cbx = NULL;
cxobj *x; cxobj *xtop = NULL; /* xpath root */
cxobj *xbot = NULL; cxobj *xbot = NULL;
cxobj *xtop = NULL; cxobj *xparent;
cxobj *xp; cxobj *x;
yang_node *y = NULL; yang_node *y = NULL;
yang_spec *yspec; yang_spec *yspec;
cxobj *xa; cxobj *xa;
char *media_content_type; char *media_content_type;
int parse_xml = 0; /* By default expect and parse JSON */ int parse_xml = 0; /* By default expect and parse JSON */
char *api_path;
clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"", clicon_debug(1, "%s api_path:\"%s\" json:\"%s\"",
__FUNCTION__, __FUNCTION__, api_path0, data);
api_path, data);
media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp); media_content_type = FCGX_GetParam("HTTP_CONTENT_TYPE", r->envp);
if (media_content_type && if (media_content_type &&
strcmp(media_content_type, "application/yang-data+xml")==0) 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"); clicon_err(OE_FATAL, 0, "No DB_SPEC");
goto done; goto done;
} }
api_path=api_path0;
for (i=0; i<pi; i++) for (i=0; i<pi; i++)
api_path = index(api_path+1, '/'); api_path = index(api_path+1, '/');
/* Create config top-of-tree */ /* Create config top-of-tree */
if ((xtop = xml_new("config", NULL, NULL)) == NULL) if ((xtop = xml_new("config", NULL, NULL)) == NULL)
goto done; goto done;
/* Translate api_path to xtop/xbot */
xbot = xtop; xbot = xtop;
if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0) if (api_path && api_path2xml(api_path, yspec, xtop, 0, &xbot, &y) < 0)
goto done; goto done;
@ -579,22 +639,48 @@ api_data_put(clicon_handle h,
badrequest(r); badrequest(r);
goto ok; goto ok;
} }
/* The message-body MUST contain exactly one instance of the
* expected data resource.
*/
if (xml_child_nr(xdata) != 1){ if (xml_child_nr(xdata) != 1){
badrequest(r); badrequest(r);
goto ok; goto ok;
} }
x = xml_child_i(xdata,0); x = xml_child_i(xdata,0);
/* Add operation (create/replace) as attribute */
if ((xa = xml_new("operation", x, NULL)) == NULL) if ((xa = xml_new("operation", x, NULL)) == NULL)
goto done; goto done;
xml_type_set(xa, CX_ATTR); xml_type_set(xa, CX_ATTR);
if (xml_value_set(xa, xml_operation2str(op)) < 0) if (xml_value_set(xa, xml_operation2str(op)) < 0)
goto done; goto done;
/* XXX Special case path=/restconf/data xml_name(x) == data */ #if 1 /* This is different from POST */
/* Replace xbot with x */ /* Replace xparent with x, ie bottom of api-path with data */
xp = xml_parent(xbot); if (api_path==NULL && strcmp(xml_name(x),"data")==0){
xml_purge(xbot); if (xml_addsub(NULL, x) < 0)
if (xml_addsub(xp, x) < 0) goto done;
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) if ((cbx = cbuf_new()) == NULL)
goto done; goto done;
if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0) if (clicon_xml2cbuf(cbx, xtop, 0, 0) < 0)
@ -628,8 +714,7 @@ api_data_put(clicon_handle h,
if (cbx) if (cbx)
cbuf_free(cbx); cbuf_free(cbx);
return retval; return retval;
} /* api_data_put */
}
/*! Generic REST PATCH method /*! Generic REST PATCH method
* @param[in] h CLIXON handle * @param[in] h CLIXON handle
@ -724,6 +809,20 @@ api_data_delete(clicon_handle h,
return retval; 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 /*! REST operation POST method
* @param[in] h CLIXON handle * @param[in] h CLIXON handle
* @param[in] r Fastcgi request 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); cvec *qvec, char *data);
int api_data_delete(clicon_handle h, FCGX_Request *r, char *api_path, int pi); 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, int api_operation_post(clicon_handle h, FCGX_Request *r,
char *path, char *path,
cvec *pcvec, int pi, cvec *qvec, char *data); cvec *pcvec, int pi, cvec *qvec, char *data);

View file

@ -748,14 +748,19 @@ text_modify_top(cxobj *x0,
cxobj *x0c; /* base child */ cxobj *x0c; /* base child */
cxobj *x1c; /* mod child */ cxobj *x1c; /* mod child */
yang_stmt *yc; /* yang child */ yang_stmt *yc; /* yang child */
char *opstr;
/* Assure top-levels are 'config' */ /* Assure top-levels are 'config' */
assert(x0 && strcmp(xml_name(x0),"config")==0); assert(x0 && strcmp(xml_name(x0),"config")==0);
assert(x1 && strcmp(xml_name(x1),"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/> */ /* Special case if x1 is empty, top-level only <config/> */
if (!xml_child_nr(x1)){ /* base tree not empty */ if (!xml_child_nr(x1)){
if (xml_child_nr(x0)) if (xml_child_nr(x0)) /* base tree not empty */
switch(op){ switch(op){
case OP_DELETE: case OP_DELETE:
case OP_REMOVE: case OP_REMOVE:

View file

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

View file

@ -2,7 +2,7 @@
# Transactions per second for large lists read/write plotter using gnuplot # Transactions per second for large lists read/write plotter using gnuplot
# #
. ./lib.sh . ./lib.sh
max=1000 # Nr of db entries max=200 # Nr of db entries
step=100 step=100
reqs=1000 reqs=1000
cfg=$dir/scaling-conf.xml cfg=$dir/scaling-conf.xml
@ -48,27 +48,55 @@ EOF
run(){ run(){
nr=$1 # Number of entries in DB nr=$1 # Number of entries in DB
reqs=$2 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 for (( i=0; i<$nr; i++ )); do
echo -n "<c>$i</c>" >> $fconfig
echo -n "<y><a>$i</a><b>$i</b></y>" >> $fconfig echo -n "<y><a>$i</a><b>$i</b></y>" >> $fconfig
done done
echo "</x></config></edit-config></rpc>]]>]]>" >> $fconfig echo "</x></config></edit-config></rpc>]]>]]>" >> $fconfig
expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^<rpc-reply><ok/></rpc-reply>]]>]]>$" expecteof_file "$clixon_netconf -qf $cfg -y $fyang" "$fconfig" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
if $write; then case $mode in
time -p for (( i=0; i<$reqs; i++ )); do readlist)
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
time -p for (( i=0; i<$reqs; i++ )); do time -p for (( i=0; i<$reqs; i++ )); do
rnd=$(( ( RANDOM % $nr ) )) rnd=$(( ( RANDOM % $nr ) ))
echo "<rpc><get-config><source><candidate/></source><filter type=\"xpath\" select=\"/x/y[a=$rnd][b=$rnd]\" /></get-config></rpc>]]>]]>" 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 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()( once()(
@ -84,18 +112,19 @@ once()(
err err
fi 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 # Actual steps
for (( i=$step; i<=$max; i=i+$step )); do for (( i=$step; i<=$max; i=i+$step )); do
t=$(TEST=%e run $i $reqs true $ 2>&1 | awk '/real/ {print $2}') step $i readlist
# t is time in secs of $reqs -> transactions per second. $reqs step $i readleaflist
p=$(echo "$reqs/$t" | bc -lq) step $i writelist
# p is transactions per second. step $i writeleaflist
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
done done
# Check if still alive # 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 xlabel "entries"
set ylabel "transactions per second" set ylabel "transactions per second"
set terminal wxt enhanced title "CLixon transactions " persist raise 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 EOF
rm -rf $dir 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" new "restconf root discovery"
expectfn "curl -sS -X GET http://localhost/.well-known/host-meta" "<Link rel='restconf' href='/restconf'/>" 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" new "restconf empty rpc"
expectfn 'curl -sS -X POST -d {"input":{"name":""}} http://localhost/restconf/operations/ex:empty' '{"output": null}' 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" new "restconf get empty config + state json"
expectfn "curl -sSG http://localhost/restconf/data" "{\"data\": $state}" 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.";
}
}
}