First working prototype

This commit is contained in:
Olof hagsand 2020-10-27 09:20:30 +01:00
parent 6b357dc038
commit 2e857bb417
16 changed files with 968 additions and 28 deletions

View file

@ -533,6 +533,14 @@ Developers may need to change their code
* See [namespace prefix nc is not supported in full #154](https://github.com/clicon/clixon/issues/154) * See [namespace prefix nc is not supported in full #154](https://github.com/clicon/clixon/issues/154)
* Fixed [Clixon backend generates wrong XML on empty string value #144](https://github.com/clicon/clixon/issues/144) * Fixed [Clixon backend generates wrong XML on empty string value #144](https://github.com/clicon/clixon/issues/144)
### New features
* Prototype of collection draft
* This is prototype work for ietf netconf work
* See draft-ietf-netconf-restconf-collection-00.txt
* New yang: ietf-restconf-collection@2020-10-22.yang
* New http media: application/yang.collection+xml/json
## 4.8.0 ## 4.8.0
18 October 2020 18 October 2020

View file

@ -1335,6 +1335,305 @@ from_client_kill_session(clicon_handle h,
return retval; return retval;
} }
/*! Help function for parsing restconf query parameter and setting netconf attribute
*
* If not "unbounded", parse and set a numeric value
* @param[in] h Clixon handle
* @param[in] name Name of attribute
* @param[in,out] cbret Output buffer for internal RPC message
* @param[out] value Value
* @retval -1 Error
* @retval 0 Invalid, cbret set
* @retval 1 OK
*/
static int
element2value(clicon_handle h,
cxobj *xe,
char *name,
cbuf *cbret,
uint32_t *value)
{
int retval = -1;
char *valstr;
int ret;
char *reason = NULL;
cxobj *x;
*value = 0;
if ((x = xml_find_type(xe, NULL, name, CX_ELMNT)) != NULL &&
(valstr = xml_body(x)) != NULL &&
strcmp(valstr, "unbounded") != 0){
if ((ret = parse_uint32(valstr, value, &reason)) < 0){
clicon_err(OE_XML, errno, "parse_uint32");
goto done;
}
if (ret == 0){
if (netconf_bad_attribute(cbret, "application",
"count", "Unrecognized value of count attribute") < 0)
goto done;
goto fail;
}
}
retval = 1;
done:
if (reason)
free(reason);
return retval;
fail:
retval = 0;
goto done;
}
/*! Retrieve collection configuration and device state information
*
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @param[in] arg client-entry
* @param[in] regarg User argument given at rpc_callback_register()
* @retval 0 OK
* @retval -1 Error
*
* @see from_client_get
*/
static int
from_client_get_collection(clicon_handle h,
cxobj *xe,
cbuf *cbret,
void *arg,
void *regarg)
{
int retval = -1;
cxobj *x;
char *xpath = NULL;
cxobj *xret = NULL;
cxobj **xvec = NULL;
size_t xlen;
cxobj *xnacm = NULL;
char *username;
cvec *nsc = NULL; /* Create a netconf namespace context from filter */
char *attr;
netconf_content content = CONTENT_ALL;
int32_t depth = -1; /* Nr of levels to print, -1 is all, 0 is none */
yang_stmt *yspec;
int i;
cxobj *xerr = NULL;
int ret;
uint32_t count = 0;
uint32_t skip = 0;
char *direction = NULL;
char *sort = NULL;
char *where = NULL;
char *datastore = NULL;
char *reason = NULL;
cbuf *cb = NULL;
cxobj *xtop = NULL;
cxobj *xbot = NULL;
yang_stmt *y;
char *api_path = NULL;
char *ns;
clicon_debug(1, "%s", __FUNCTION__);
username = clicon_username_get(h);
if ((yspec = clicon_dbspec_yang(h)) == NULL){
clicon_err(OE_YANG, ENOENT, "No yang spec9");
goto done;
}
/* Clixon extensions: content */
if ((attr = xml_find_value(xe, "content")) != NULL)
content = netconf_content_str2int(attr);
/* Clixon extensions: depth */
if ((attr = xml_find_value(xe, "depth")) != NULL){
if ((ret = parse_int32(attr, &depth, &reason)) < 0){
clicon_err(OE_XML, errno, "parse_int32");
goto done;
}
if (ret == 0){
if (netconf_bad_attribute(cbret, "application",
"depth", "Unrecognized value of depth attribute") < 0)
goto done;
goto ok;
}
}
/* count */
if ((ret = element2value(h, xe, "count", cbret, &count)) < 0)
goto done;
/* skip */
if (ret && (ret = element2value(h, xe, "skip", cbret, &skip)) < 0)
goto done;
/* direction */
if (ret && (x = xml_find_type(xe, NULL, "direction", CX_ELMNT)) != NULL){
direction = xml_body(x);
if (strcmp(direction, "forward") != 0 && strcmp(direction, "reverse") != 0){
if (netconf_bad_attribute(cbret, "application",
"direction", "Unrecognized value of direction attribute") < 0)
goto done;
goto ok;
}
}
/* sort */
if (ret && (x = xml_find_type(xe, NULL, "sort", CX_ELMNT)) != NULL)
sort = xml_body(x);
if (sort) ; /* XXX */
/* where */
if (ret && (x = xml_find_type(xe, NULL, "where", CX_ELMNT)) != NULL)
where = xml_body(x);
/* datastore */
if (ret && (x = xml_find_type(xe, NULL, "datastore", CX_ELMNT)) != NULL)
datastore = xml_body(x);
if (ret == 0)
goto ok;
/* list-target, create (xml and) yang to check if is state (CF) or config (CT) */
if (ret && (x = xml_find_type(xe, NULL, "list-target", CX_ELMNT)) != NULL)
api_path = xml_body(x);
if ((xtop = xml_new("top", NULL, CX_ELMNT)) == NULL)
goto done;
if ((ret = api_path2xml(api_path, yspec, xtop, YC_DATANODE, 0, &xbot, &y, &xerr)) < 0)
goto done;
if (ret == 0){
if (clicon_xml2cbuf(cbret, xerr, 0, 0, -1) < 0)
goto done;
goto ok;
}
if (yang_keyword_get(y) != Y_LIST && yang_keyword_get(y) != Y_LEAF_LIST){
if (netconf_bad_element(cbret, "application", "list-target", "path invalid") < 0)
goto done;
goto ok;
}
/* Create xpath from api-path for the datastore API */
if ((ret = api_path2xpath(api_path, yspec, &xpath, &nsc, &xerr)) < 0)
goto done;
if (ret == 0){
if (clicon_xml2cbuf(cbret, xerr, 0, 0, -1) < 0)
goto done;
goto ok;
}
/* Build a "predicate" cbuf */
if ((cb = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
cprintf(cb, "%s", xpath);
if (where)
cprintf(cb, "[%s]", where);
if (skip){
cprintf(cb, "[%u < position()", skip);
if (count)
cprintf(cb, " && position() < %u", count+skip);
cprintf(cb, "]");
}
else if (count)
cprintf(cb, "[position() < %u]", count);
/* Split into CT or CF */
if (yang_config_ancestor(y) == 1){ /* CT */
if (content == CONTENT_CONFIG || content == CONTENT_ALL){
if (xmldb_get0(h, datastore, YB_MODULE, nsc, cbuf_get(cb), 1, &xret, NULL) < 0) {
if (netconf_operation_failed(cbret, "application", "read registry")< 0)
goto done;
goto ok;
}
}
/* There may be CF data in a CT collection */
if (content == CONTENT_ALL){
if ((ret = client_statedata(h, cbuf_get(cb), nsc, content, &xret)) < 0)
goto done;
}
}
else { /* CF */
/* There can be no CT data in a CF collection */
if (content == CONTENT_NONCONFIG || content == CONTENT_ALL){
if ((ret = client_statedata(h, cbuf_get(cb), nsc, content, &xret)) < 0)
goto done;
if (ret == 0){ /* Error from callback (error in xret) */
if (clicon_xml2cbuf(cbret, xret, 0, 0, -1) < 0)
goto done;
goto ok;
}
}
}
if (clicon_option_bool(h, "CLICON_VALIDATE_STATE_XML")){
/* Check XML by validating it. return internal error with error cause
* Primarily intended for user-supplied state-data.
* The whole config tree must be present in case the state data references config data
*/
if ((ret = xml_yang_validate_all_top(h, xret, &xerr)) < 0)
goto done;
if (ret > 0 &&
(ret = xml_yang_validate_add(h, xret, &xerr)) < 0)
goto done;
if (ret == 0){
if (clicon_debug_get())
clicon_log_xml(LOG_DEBUG, xret, "VALIDATE_STATE");
if (clixon_netconf_internal_error(xerr,
". Internal error, state callback returned invalid XML",
NULL) < 0)
goto done;
if (clicon_xml2cbuf(cbret, xerr, 0, 0, -1) < 0)
goto done;
goto ok;
}
} /* CLICON_VALIDATE_STATE_XML */
if (content == CONTENT_NONCONFIG){ /* state only, all config should be removed now */
/* Keep state data only, remove everything that is not config. Note that state data
* may be a sub-part in a config tree, we need to traverse to find all
*/
if (xml_non_config_data(xret, NULL) < 0)
goto done;
if (xml_tree_prune_flagged_sub(xret, XML_FLAG_MARK, 1, NULL) < 0)
goto done;
if (xml_apply(xret, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0)
goto done;
}
/* Code complex to filter out anything that is outside of xpath
* Actually this is a safety catch, should really be done in plugins
* and modules_state functions.
*/
if (xpath_vec(xret, nsc, "%s", &xvec, &xlen, xpath?xpath:"/") < 0)
goto done;
/* Pre-NACM access step */
xnacm = clicon_nacm_cache(h);
if (xnacm != NULL){ /* Do NACM validation */
/* NACM datanode/module read validation */
if (nacm_datanode_read(h, xret, xvec, xlen, username, xnacm) < 0)
goto done;
}
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><collection xmlns=\"%s\">",
NETCONF_BASE_NAMESPACE, NETCONF_COLLECTION_NAMESPACE); /* OK */
if ((ns = yang_find_mynamespace(y)) != NULL)
for (i=0; i<xlen; i++){
x = xvec[i];
/* Add namespace */
if (xmlns_set(x, NULL, ns) < 0)
goto done;
/* Top level is data, so add 1 to depth if significant */
if (clicon_xml2cbuf(cbret, x, 0, 0, depth>0?depth+1:depth) < 0)
goto done;
}
cprintf(cbret, "</collection></rpc-reply>");
ok:
retval = 0;
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
if (xtop)
xml_free(xtop);
if (cb)
cbuf_free(cb);
if (reason)
free(reason);
if (xerr)
xml_free(xerr);
if (xvec)
free(xvec);
if (nsc)
xml_nsctx_free(nsc);
if (xret)
xml_free(xret);
return retval;
}
/*! Create a notification subscription /*! Create a notification subscription
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc> * @param[in] xe Request: <rpc><xn></rpc>
@ -1960,7 +2259,10 @@ backend_rpc_init(clicon_handle h)
if (rpc_callback_register(h, from_client_validate, NULL, if (rpc_callback_register(h, from_client_validate, NULL,
NETCONF_BASE_NAMESPACE, "validate") < 0) NETCONF_BASE_NAMESPACE, "validate") < 0)
goto done; goto done;
/* draft-ietf-netconf-restconf-collection-00 */
if (rpc_callback_register(h, from_client_get_collection, NULL,
NETCONF_COLLECTION_NAMESPACE, "get-collection") < 0)
goto done;
/* In backend_client.? RPC from RFC 5277 */ /* In backend_client.? RPC from RFC 5277 */
if (rpc_callback_register(h, from_client_create_subscription, NULL, if (rpc_callback_register(h, from_client_create_subscription, NULL,
EVENT_RFC5277_NAMESPACE, "create-subscription") < 0) EVENT_RFC5277_NAMESPACE, "create-subscription") < 0)

View file

@ -42,13 +42,19 @@ extern "C" {
#define _CLIXON_RESTCONF_H_ #define _CLIXON_RESTCONF_H_
/* /*
* Types (also in restconf_lib.h) * Types
*/
/*! RESTCONF media types
* @see http_media_map
* @note DUPLICATED in clixon_lib.h
*/ */
enum restconf_media{ enum restconf_media{
YANG_DATA_JSON, /* "application/yang-data+json" */ YANG_DATA_JSON, /* "application/yang-data+json" */
YANG_DATA_XML, /* "application/yang-data+xml" */ YANG_DATA_XML, /* "application/yang-data+xml" */
YANG_PATCH_JSON, /* "application/yang-patch+json" */ YANG_PATCH_JSON, /* "application/yang-patch+json" */
YANG_PATCH_XML /* "application/yang-patch+xml" */ YANG_PATCH_XML, /* "application/yang-patch+xml" */
YANG_COLLECTION_XML, /* draft-ietf-netconf-restconf-collection-00.txt */
YANG_COLLECTION_JSON /* draft-ietf-netconf-restconf-collection-00.txt */
}; };
typedef enum restconf_media restconf_media; typedef enum restconf_media restconf_media;

View file

@ -275,6 +275,7 @@ api_return_err(clicon_handle h,
switch (media){ switch (media){
case YANG_DATA_XML: case YANG_DATA_XML:
case YANG_PATCH_XML: case YANG_PATCH_XML:
case YANG_COLLECTION_XML:
clicon_debug(1, "%s code:%d", __FUNCTION__, code); clicon_debug(1, "%s code:%d", __FUNCTION__, code);
if (pretty){ if (pretty){
cprintf(cb, " <errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n"); cprintf(cb, " <errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">\n");
@ -291,6 +292,7 @@ api_return_err(clicon_handle h,
break; break;
case YANG_DATA_JSON: case YANG_DATA_JSON:
case YANG_PATCH_JSON: case YANG_PATCH_JSON:
case YANG_COLLECTION_JSON:
clicon_debug(1, "%s code:%d", __FUNCTION__, code); clicon_debug(1, "%s code:%d", __FUNCTION__, code);
if (pretty){ if (pretty){
cprintf(cb, "{\n\"ietf-restconf:errors\" : "); cprintf(cb, "{\n\"ietf-restconf:errors\" : ");
@ -306,9 +308,7 @@ api_return_err(clicon_handle h,
cprintf(cb, "}\r\n"); cprintf(cb, "}\r\n");
} }
break; break;
default: default: /* Just ignore the body so that there is a reply */
clicon_err(OE_YANG, EINVAL, "Invalid media type %d", media);
goto done;
break; break;
} /* switch media */ } /* switch media */
assert(cbuf_len(cb)); assert(cbuf_len(cb));

View file

@ -207,6 +207,8 @@ static const map_str2int http_media_map[] = {
{"application/yang-data+json", YANG_DATA_JSON}, {"application/yang-data+json", YANG_DATA_JSON},
{"application/yang-patch+xml", YANG_PATCH_XML}, {"application/yang-patch+xml", YANG_PATCH_XML},
{"application/yang-patch+json", YANG_PATCH_JSON}, {"application/yang-patch+json", YANG_PATCH_JSON},
{"application/yang.collection+xml", YANG_COLLECTION_XML},
{"application/yang.collection+json", YANG_COLLECTION_JSON},
{NULL, -1} {NULL, -1}
}; };

View file

@ -46,13 +46,15 @@ extern "C" {
*/ */
/*! RESTCONF media types /*! RESTCONF media types
* @see http_media_map * @see http_media_map
* (also in clixon_restconf.h) * @note DUPLICATED in clixon_restconf.h
*/ */
enum restconf_media{ enum restconf_media{
YANG_DATA_JSON, /* "application/yang-data+json" */ YANG_DATA_JSON, /* "application/yang-data+json" */
YANG_DATA_XML, /* "application/yang-data+xml" */ YANG_DATA_XML, /* "application/yang-data+xml" */
YANG_PATCH_JSON, /* "application/yang-patch+json" */ YANG_PATCH_JSON, /* "application/yang-patch+json" */
YANG_PATCH_XML /* "application/yang-patch+xml" */ YANG_PATCH_XML, /* "application/yang-patch+xml" */
YANG_COLLECTION_XML, /* draft-ietf-netconf-restconf-collection-00.txt */
YANG_COLLECTION_JSON /* draft-ietf-netconf-restconf-collection-00.txt */
}; };
typedef enum restconf_media restconf_media; typedef enum restconf_media restconf_media;

View file

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

View file

@ -432,6 +432,7 @@ api_root_restconf(clicon_handle h,
cbuf *cb = NULL; cbuf *cb = NULL;
char *media_str = NULL; char *media_str = NULL;
restconf_media media_out = YANG_DATA_JSON; restconf_media media_out = YANG_DATA_JSON;
restconf_media media_in;
char *indata = NULL; char *indata = NULL;
char *username = NULL; char *username = NULL;
int ret; int ret;
@ -468,7 +469,6 @@ api_root_restconf(clicon_handle h,
goto ok; goto ok;
} }
} }
clicon_debug(1, "%s ACCEPT: %s %s", __FUNCTION__, media_str, restconf_media_int2str(media_out)); clicon_debug(1, "%s ACCEPT: %s %s", __FUNCTION__, media_str, restconf_media_int2str(media_out));
if ((pvec = clicon_strsep(path, "/", &pn)) == NULL) if ((pvec = clicon_strsep(path, "/", &pn)) == NULL)

View file

@ -55,6 +55,7 @@ int clicon_rpc_delete_config(clicon_handle h, char *db);
int clicon_rpc_lock(clicon_handle h, char *db); int clicon_rpc_lock(clicon_handle h, char *db);
int clicon_rpc_unlock(clicon_handle h, char *db); int clicon_rpc_unlock(clicon_handle h, char *db);
int clicon_rpc_get(clicon_handle h, char *xpath, cvec *nsc, netconf_content content, int32_t depth, cxobj **xret); int clicon_rpc_get(clicon_handle h, char *xpath, cvec *nsc, netconf_content content, int32_t depth, cxobj **xret);
int clicon_rpc_get_collection(clicon_handle h, char *apipath, yang_stmt *yco, cvec *nsc, netconf_content content, char *depth, char *count, char *skip, char *direction, char *sort, char *where, cxobj **xt);
int clicon_rpc_close_session(clicon_handle h); int clicon_rpc_close_session(clicon_handle h);
int clicon_rpc_kill_session(clicon_handle h, uint32_t session_id); int clicon_rpc_kill_session(clicon_handle h, uint32_t session_id);
int clicon_rpc_validate(clicon_handle h, char *db); int clicon_rpc_validate(clicon_handle h, char *db);

View file

@ -54,6 +54,10 @@
*/ */
#define NETCONF_INPUT_CONFIG "config" #define NETCONF_INPUT_CONFIG "config"
/* Collections namespace from draft-ietf-netconf-restconf-collection-00.txt
*/
#define NETCONF_COLLECTION_NAMESPACE "urn:ietf:params:xml:ns:yang:ietf-netconf-collection"
/* Output symbol for netconf get/get-config /* Output symbol for netconf get/get-config
* ietf-netconf.yang defines it as output: * ietf-netconf.yang defines it as output:
* output { anyxml data; * output { anyxml data;

View file

@ -1526,6 +1526,10 @@ netconf_module_load(clicon_handle h)
if (clicon_option_bool(h, "CLICON_NETCONF_MESSAGE_ID_OPTIONAL") == 1) if (clicon_option_bool(h, "CLICON_NETCONF_MESSAGE_ID_OPTIONAL") == 1)
xml_bind_netconf_message_id_optional(1); xml_bind_netconf_message_id_optional(1);
#endif #endif
/* Load restconf collection */
if (yang_spec_parse_module(h, "ietf-netconf-collection", NULL, yspec)< 0)
goto done;
retval = 0; retval = 0;
done: done:
return retval; return retval;

View file

@ -829,7 +829,8 @@ api_path2xpath_cvv(cvec *api_path,
* @code * @code
* char *xpath = NULL; * char *xpath = NULL;
* cvec *nsc = NULL; * cvec *nsc = NULL;
* if ((ret = api_path2xpath("/module:a/b", yspec, &xpath, &nsc)) < 0) * cxobj *xerr = NULL;
* if ((ret = api_path2xpath("/module:a/b", yspec, &xpath, &nsc, &xerr)) < 0)
* err; * err;
* if (ret == 1) * if (ret == 1)
* ... access xpath as cbuf_get(xpath) * ... access xpath as cbuf_get(xpath)
@ -896,6 +897,7 @@ api_path2xpath(char *api_path,
* @param[in] x0 Xpath tree so far * @param[in] x0 Xpath tree so far
* @param[in] y0 Yang spec for x0 * @param[in] y0 Yang spec for x0
* @param[in] nodeclass Set to schema nodes, data nodes, etc * @param[in] nodeclass Set to schema nodes, data nodes, etc
* @param[in] strict Break if api-path is not "complete" otherwise ignore and continue
* @param[out] xbotp Resulting xml tree * @param[out] xbotp Resulting xml tree
* @param[out] ybotp Yang spec matching xpathp * @param[out] ybotp Yang spec matching xpathp
* @param[out] xerr Netconf error message (if retval=0) * @param[out] xerr Netconf error message (if retval=0)
@ -1019,10 +1021,6 @@ api_path2xml_vec(char **vec,
break; break;
case Y_LIST: case Y_LIST:
cvk = yang_cvec_get(y); /* Use Y_LIST cache, see ys_populate_list() */ cvk = yang_cvec_get(y); /* Use Y_LIST cache, see ys_populate_list() */
if (valvec){ /* loop, valvec may have been used before */
free(valvec);
valvec = NULL;
}
if (restval==NULL){ if (restval==NULL){
if (strict){ if (strict){
cprintf(cberr, "malformed key =%s, expected '=restval'", nodeid); cprintf(cberr, "malformed key =%s, expected '=restval'", nodeid);
@ -1142,6 +1140,7 @@ api_path2xml_vec(char **vec,
* @param[in] yspec Yang spec * @param[in] yspec Yang spec
* @param[in,out] xtop Incoming XML tree * @param[in,out] xtop Incoming XML tree
* @param[in] nodeclass Set to schema nodes, data nodes, etc * @param[in] nodeclass Set to schema nodes, data nodes, etc
* @param[in] strict Break if api-path is not "complete" otherwise ignore and continue
* @param[out] xbotp Resulting xml tree (end of xpath) * @param[out] xbotp Resulting xml tree (end of xpath)
* @param[out] ybotp Yang spec matching xbotp * @param[out] ybotp Yang spec matching xbotp
* @param[out] xerr Netconf error message (if retval=0) * @param[out] xerr Netconf error message (if retval=0)
@ -1163,6 +1162,7 @@ api_path2xml_vec(char **vec,
* @endcode * @endcode
* @note "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3 * @note "api-path" is "URI-encoded path expression" definition in RFC8040 3.5.3
* @see api_path2xpath For api-path to xpath translation (maybe could be combined?) * @see api_path2xpath For api-path to xpath translation (maybe could be combined?)
* @note "Collections" should use strict = 0
*/ */
int int
api_path2xml(char *api_path, api_path2xml(char *api_path,

View file

@ -823,6 +823,7 @@ clicon_rpc_get(clicon_handle h,
if (depth != -1) if (depth != -1)
cprintf(cb, " depth=\"%d\"", depth); cprintf(cb, " depth=\"%d\"", depth);
cprintf(cb, ">"); cprintf(cb, ">");
/* If xpath, add a filter */
if (xpath && strlen(xpath)) { if (xpath && strlen(xpath)) {
cprintf(cb, "<%s:filter %s:type=\"xpath\" %s:select=\"%s\"", cprintf(cb, "<%s:filter %s:type=\"xpath\" %s:select=\"%s\"",
NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX, NETCONF_BASE_PREFIX,
@ -877,7 +878,134 @@ clicon_rpc_get(clicon_handle h,
return retval; return retval;
} }
/*! Get database configuration and state data collection
* @param[in] h Clicon handle
* @param[in] apipath To identify a list/leaf-list
* @param[in] yli Yang-stmt of list/leaf-list of collection
* @param[in] namespace Namespace associated w xpath
* @param[in] nsc Namespace context for filter
* @param[in] content Clixon extension: all, config, noconfig. -1 means all
* @param[in] depth Nr of XML levels to get, -1 is all, 0 is none
* @param[in] count Collection/clixon extension
* @param[in] skip Collection/clixon extension
* @param[in] direction Collection/clixon extension
* @param[in] sort Collection/clixon extension
* @param[in] where Collection/clixon extension
* @param[out] xt XML tree. Free with xml_free.
* Either <config> or <rpc-error>.
* @retval 0 OK
* @retval -1 Error, fatal or xml
* @see clicon_rpc_get
* @see draft-ietf-netconf-restconf-collection-00
* @note the netconf return message is yang populated, as well as the return data
*/
int
clicon_rpc_get_collection(clicon_handle h,
char *apipath,
yang_stmt *yli,
cvec *nsc, /* namespace context for filter */
netconf_content content,
char *depth,
char *count,
char *skip,
char *direction,
char *sort,
char *where,
cxobj **xt)
{
int retval = -1;
struct clicon_msg *msg = NULL;
cbuf *cb = NULL;
cxobj *xret = NULL;
cxobj *xerr = NULL;
cxobj *xr;
char *username;
uint32_t session_id;
int ret;
yang_stmt *yspec;
cxobj *x;
if (session_id_check(h, &session_id) < 0)
goto done;
if ((cb = cbuf_new()) == NULL)
goto done;
cprintf(cb, "<rpc xmlns=\"%s\" ", NETCONF_BASE_NAMESPACE);
if ((username = clicon_username_get(h)) != NULL)
cprintf(cb, " username=\"%s\"", username);
cprintf(cb, " xmlns:%s=\"%s\"",
NETCONF_BASE_PREFIX, NETCONF_BASE_NAMESPACE);
cprintf(cb, "><get-collection xmlns=\"%s\"", NETCONF_COLLECTION_NAMESPACE);
/* Clixon extension, content=all,config, or nonconfig */
if ((int)content != -1)
cprintf(cb, " content=\"%s\"", netconf_content_int2str(content));
if (depth)
cprintf(cb, " depth=\"%s\"", depth);
cprintf(cb, ">");
if (count)
cprintf(cb, "<list-target>%s</list-target>", apipath);
if (count)
cprintf(cb, "<count>%s</count>", count);
if (skip)
cprintf(cb, "<skip>%s</skip>", skip);
if (direction)
cprintf(cb, "<direction>%s</direction>", direction);
if (sort)
cprintf(cb, "<sort>%s</sort>", sort);
if (where)
cprintf(cb, "<where>%s</where>", where);
cprintf(cb, "</get-collection></rpc>");
if ((msg = clicon_msg_encode(session_id, "%s", cbuf_get(cb))) == NULL)
goto done;
if (clicon_rpc_msg(h, msg, &xret, NULL) < 0)
goto done;
/* Send xml error back: first check error, then ok */
if ((xr = xpath_first(xret, NULL, "/rpc-reply/rpc-error")) != NULL)
xr = xml_parent(xr); /* point to rpc-reply */
else if ((xr = xpath_first(xret, NULL, "/rpc-reply/collection")) == NULL){
if ((xr = xml_new("collection", NULL, CX_ELMNT)) == NULL)
goto done;
}
else{
yspec = clicon_dbspec_yang(h);
/* Populate all children with yco */
x = NULL;
while ((x = xml_child_each(xr, x, CX_ELMNT)) != NULL){
xml_spec_set(x, yli);
if ((ret = xml_bind_yang(x, YB_PARENT, yspec, &xerr)) < 0)
goto done;
if (ret == 0){
if (clixon_netconf_internal_error(xerr,
". Internal error, backend returned invalid XML.",
NULL) < 0)
goto done;
if ((xr = xpath_first(xerr, NULL, "rpc-error")) == NULL){
clicon_err(OE_XML, ENOENT, "Expected rpc-error tag but none found(internal)");
goto done;
}
}
}
}
if (xr){
if (xml_rm(xr) < 0)
goto done;
*xt = xr;
}
retval = 0;
done:
if (cb)
cbuf_free(cb);
if (xerr)
xml_free(xerr);
if (xret)
xml_free(xret);
if (msg)
free(msg);
return retval;
}
/*! Send a close a netconf user session. Socket is also closed if still open /*! Send a close a netconf user session. Socket is also closed if still open
*
* @param[in] h CLICON handle * @param[in] h CLICON handle
* @retval 0 OK * @retval 0 OK
* @retval -1 Error and logged to syslog * @retval -1 Error and logged to syslog

View file

@ -743,4 +743,3 @@ xml_bind_yang_rpc_reply(cxobj *xrpc,
retval = 0; retval = 0;
goto done; goto done;
} }

114
test/test_collection.sh Executable file
View file

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

View file

@ -0,0 +1,125 @@
module ietf-netconf-collection {
namespace "urn:ietf:params:xml:ns:yang:ietf-netconf-collection";
prefix "rcoll";
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: <http://tools.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 conceptual YANG specifications
for the RESTCONF Collection resource type.
Note that the YANG definitions within this module do not
represent configuration data of any kind.
The YANG grouping statements provide a normative syntax
for XML and JSON message encoding purposes.
Copyright (c) 2015 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 XXXX; see
the RFC itself for full legal notices.";
/*
module: ietf-netconf-collection
rpcs:
+---x get-collection
+--w input
+--w module-name string
+--w datastore string
+--w list-target string
+--w count uint32
+--w skip uint32
+--w direction enum
+--w sort string
+--w where string
+--ro output
+-ro collection anydata
*/
revision 2020-10-22 {
description
"Draft by Olof Hagsand / Clixon.";
}
rpc get-collection {
input {
leaf module-name {
/* Not needed with proper list-target */
type string;
}
leaf datastore {
type string;
default "running";
}
leaf list-target {
description "api-path";
mandatory true;
type string;
}
leaf count {
type union {
type uint32;
type string {
pattern 'unbounded';
}
}
}
leaf skip {
type union {
type uint32;
type string {
pattern 'unbounded';
}
}
}
leaf direction {
type enumeration {
enum forward;
enum reverse;
}
}
leaf sort {
type string;
}
leaf where {
type string;
}
}
output {
anyxml collection {
description
"Copy of the running datastore subset and/or state
data that matched the filter criteria (if any).
An empty data container indicates that the request did not
produce any results.";
}
}
}
}