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