Replaced strchr with nodeid_split
Experimental code for replacing identityref lists with module:id instead of prefix:id
This commit is contained in:
parent
bb04f778ab
commit
e44685a100
13 changed files with 348 additions and 182 deletions
|
|
@ -99,11 +99,14 @@ However, the following YANG syntax modules are not implemented (reference to RFC
|
|||
- deviation (7.20.3)
|
||||
- action (7.15)
|
||||
- augment in a uses sub-clause (7.17) (module-level augment is implemented)
|
||||
- require-instance
|
||||
- instance-identifier type
|
||||
- status (7.21.2)
|
||||
- extension (7.19)
|
||||
- extension (7.19) supported syntactically, but no hooks/plugins for extenstions
|
||||
- YIN (13)
|
||||
- Yang extended Xpath functions: re-match(), deref)(), derived-from(), derived-from-or-self(), enum-value(), bit-is-set() (10.2-10.6)
|
||||
- Default values on leaf-lists are not supported (7.7.2)
|
||||
- instance-identifier type
|
||||
|
||||
### Yang patterns
|
||||
Yang type patterns use regexps defined in [W3C XML XSD](http://www.w3.org/TR/2004/REC-xmlschema-2-20041028). XSD regexp:s are
|
||||
|
|
|
|||
|
|
@ -184,6 +184,40 @@ cli_signal_flush(clicon_handle h)
|
|||
cli_signal_block (h);
|
||||
}
|
||||
|
||||
/*! Transform data
|
||||
* Add next-last cvv (resolved variable) as body to xml bottom for leaf and
|
||||
* leaf-list.
|
||||
* There may be some translation necessary.
|
||||
*/
|
||||
static int
|
||||
dbxml_body(cxobj *xbot,
|
||||
yang_stmt *ybot,
|
||||
cvec *cvv)
|
||||
{
|
||||
int retval = -1;
|
||||
char *str = NULL;
|
||||
cxobj *xb;
|
||||
cg_var *cval;
|
||||
int len;
|
||||
|
||||
len = cvec_len(cvv);
|
||||
cval = cvec_i(cvv, len-1);
|
||||
if ((str = cv2str_dup(cval)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cv2str_dup");
|
||||
goto done;
|
||||
}
|
||||
if ((xb = xml_new("body", xbot, NULL)) == NULL)
|
||||
goto done;
|
||||
xml_type_set(xb, CX_BODY);
|
||||
if (xml_value_set(xb, str) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
if (str)
|
||||
free(str);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Modify xml datastore from a callback using xml key format strings
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] cvv Vector of cli string and instantiated variables
|
||||
|
|
@ -206,11 +240,11 @@ cli_dbxml(clicon_handle h,
|
|||
enum operation_type op)
|
||||
{
|
||||
int retval = -1;
|
||||
char *str = NULL;
|
||||
// char *str = NULL;
|
||||
char *api_path_fmt; /* xml key format */
|
||||
char *api_path = NULL; /* xml key */
|
||||
cg_var *cval;
|
||||
int len;
|
||||
// cg_var *cval;
|
||||
// int len;
|
||||
cg_var *arg;
|
||||
cbuf *cb = NULL;
|
||||
yang_stmt *yspec;
|
||||
|
|
@ -218,7 +252,7 @@ cli_dbxml(clicon_handle h,
|
|||
yang_stmt *y = NULL; /* yang spec of xpath */
|
||||
cxobj *xtop = NULL; /* xpath root */
|
||||
cxobj *xa; /* attribute */
|
||||
cxobj *xb; /* body */
|
||||
// cxobj *xb; /* body */
|
||||
|
||||
if (cvec_len(argv) != 1){
|
||||
clicon_err(OE_PLUGIN, 0, "Requires one element to be xml key format string");
|
||||
|
|
@ -241,22 +275,12 @@ cli_dbxml(clicon_handle h,
|
|||
if ((xa = xml_new("operation", xbot, NULL)) == NULL)
|
||||
goto done;
|
||||
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;
|
||||
if (yang_keyword_get(y) != Y_LIST && yang_keyword_get(y) != Y_LEAF_LIST){
|
||||
len = cvec_len(cvv);
|
||||
if (len > 1){
|
||||
cval = cvec_i(cvv, len-1);
|
||||
if ((str = cv2str_dup(cval)) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cv2str_dup");
|
||||
goto done;
|
||||
}
|
||||
if ((xb = xml_new("body", xbot, NULL)) == NULL)
|
||||
goto done;
|
||||
xml_type_set(xb, CX_BODY);
|
||||
if (xml_value_set(xb, str) < 0)
|
||||
goto done;
|
||||
}
|
||||
if (cvec_len(cvv) > 1 &&
|
||||
dbxml_body(xbot, y, cvv) < 0)
|
||||
goto done;
|
||||
}
|
||||
if ((cb = cbuf_new()) == NULL){
|
||||
clicon_err(OE_XML, errno, "cbuf_new");
|
||||
|
|
@ -274,8 +298,6 @@ cli_dbxml(clicon_handle h,
|
|||
done:
|
||||
if (cb)
|
||||
cbuf_free(cb);
|
||||
if (str)
|
||||
free(str);
|
||||
if (api_path)
|
||||
free(api_path);
|
||||
if (xtop)
|
||||
|
|
|
|||
|
|
@ -530,7 +530,7 @@ yang2cli_var(clicon_handle h,
|
|||
cbuf *cb)
|
||||
{
|
||||
int retval = -1;
|
||||
char *origtype;
|
||||
char *origtype = NULL;
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype; /* resolved type */
|
||||
cvec *cvv = NULL;
|
||||
|
|
@ -596,6 +596,8 @@ yang2cli_var(clicon_handle h,
|
|||
}
|
||||
retval = 0;
|
||||
done:
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
if (patterns)
|
||||
cvec_free(patterns);
|
||||
return retval;
|
||||
|
|
|
|||
|
|
@ -48,3 +48,9 @@
|
|||
*/
|
||||
#define USE_NETCONF_NS_AS_DEFAULT
|
||||
|
||||
/* Use modulename:id instead of prefix:id in derived identityref list
|
||||
* Modulenames are global/canonical but prefixes are not.
|
||||
* Experimental since there is some mapping between prefixes and module names
|
||||
* that needs to be done
|
||||
*/
|
||||
#undef USE_IDREF_LIST_MODULE
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ typedef enum yang_class yang_class;
|
|||
/*
|
||||
* Prototypes
|
||||
*/
|
||||
int isxmlns(cxobj *x);
|
||||
int xml2txt(FILE *f, cxobj *x, int level);
|
||||
int xml2cli(FILE *f, cxobj *x, char *prepend, enum genmodel_type gt);
|
||||
int xml_yang_root(cxobj *x, cxobj **xr);
|
||||
|
|
|
|||
|
|
@ -164,8 +164,6 @@ yang_stmt *ys_dup(yang_stmt *old);
|
|||
int yn_insert(yang_stmt *ys_parent, yang_stmt *ys_child);
|
||||
yang_stmt *yn_each(yang_stmt *yn, yang_stmt *ys);
|
||||
char *yang_key2str(int keyword);
|
||||
char *yarg_prefix(yang_stmt *ys);
|
||||
char *yarg_id(yang_stmt *ys);
|
||||
int ys_module_by_xml(yang_stmt *ysp, struct xml *xt, yang_stmt **ymodp);
|
||||
yang_stmt *ys_module(yang_stmt *ys);
|
||||
yang_stmt *ys_real_module(yang_stmt *ys);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,45 @@
|
|||
#include "clixon_datastore_read.h"
|
||||
#include "clixon_datastore_tree.h"
|
||||
|
||||
/*! Replace all xmlns attributes in x0 with xmlns attributes in x1
|
||||
* This is an embryo of code to actually check if namespace binding is canonical
|
||||
* and if it is not, either return error or transform to canonical.
|
||||
* "Canonical" meaning comply to the yang module prefixes.
|
||||
* The current code does not really do anything useful
|
||||
*/
|
||||
static int
|
||||
replace_xmlns(cxobj *x0,
|
||||
cxobj *x1)
|
||||
{
|
||||
int retval = -1;
|
||||
cxobj *x = NULL;
|
||||
cxobj *xcopy;
|
||||
int i;
|
||||
|
||||
for (i=0; i<xml_child_nr(x0); ){
|
||||
x = xml_child_i(x0, i);
|
||||
if (!isxmlns(x)){
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
xml_rm(x);
|
||||
xml_free(x);
|
||||
}
|
||||
x = NULL;
|
||||
while ((x = xml_child_each(x1, x, CX_ATTR)) != NULL) {
|
||||
/* split and only add xmlns= and xmlns:x= attributes! */
|
||||
if (!isxmlns(x))
|
||||
continue;
|
||||
if ((xcopy = xml_new(xml_name(x), x0, xml_spec(x))) == NULL)
|
||||
goto done;
|
||||
if (xml_copy(x, xcopy) < 0) /* recursion */
|
||||
goto done;
|
||||
}
|
||||
retval = 0;
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Modify a base tree x0 with x1 with yang spec y according to operation op
|
||||
* @param[in] th Datastore text handle
|
||||
* @param[in] x0 Base xml tree (can be NULL in add scenarios)
|
||||
|
|
@ -187,6 +226,16 @@ text_modify(clicon_handle h,
|
|||
xml_type_set(x0b, CX_BODY);
|
||||
}
|
||||
}
|
||||
else { /* if change existing node, replace xmlns attributes
|
||||
* This is only done for leaf/leaf-list now, eg terminals
|
||||
* and is only an embryo of checking canonical namespace
|
||||
* bindings.
|
||||
* But it does catch some cornercases where a new
|
||||
* namespace binding is replacing an old for eg identityref
|
||||
*/
|
||||
if (replace_xmlns(x0, x1) < 0)
|
||||
goto done;
|
||||
}
|
||||
if (x1bstr){
|
||||
if ((x0b = xml_body_get(x0)) != NULL){
|
||||
x0bstr = xml_value(x0b);
|
||||
|
|
|
|||
|
|
@ -623,8 +623,8 @@ clicon_str2int_search(const map_str2int *mstab,
|
|||
|
||||
/*! Split colon-separated node identifier into prefix and name
|
||||
* @param[in] node-id
|
||||
* @param[out] prefix Malloced string. May be NULL.
|
||||
* @param[out] id Malloced identifier.
|
||||
* @param[out] prefix If non-NULL, return malloced string, or NULL.
|
||||
* @param[out] id If non-NULL, return malloced identifier.
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error
|
||||
* @code
|
||||
|
|
@ -647,19 +647,21 @@ nodeid_split(char *nodeid,
|
|||
char *str;
|
||||
|
||||
if ((str = strchr(nodeid, ':')) == NULL){
|
||||
if ((*id = strdup(nodeid)) == NULL){
|
||||
if (id && (*id = strdup(nodeid)) == NULL){
|
||||
clicon_err(OE_YANG, errno, "strdup");
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
else{
|
||||
if ((*prefix = strdup(nodeid)) == NULL){
|
||||
clicon_err(OE_YANG, errno, "strdup");
|
||||
goto done;
|
||||
else {
|
||||
if (prefix){
|
||||
if ((*prefix = strdup(nodeid)) == NULL){
|
||||
clicon_err(OE_YANG, errno, "strdup");
|
||||
goto done;
|
||||
}
|
||||
(*prefix)[str-nodeid] = '\0';
|
||||
}
|
||||
(*prefix)[str-nodeid] = '\0';
|
||||
str++;
|
||||
if ((*id = strdup(str)) == NULL){
|
||||
if (id && (*id = strdup(str)) == NULL){
|
||||
clicon_err(OE_YANG, errno, "strdup");
|
||||
goto done;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,22 @@
|
|||
#include "clixon_yang_type.h"
|
||||
#include "clixon_xml_map.h"
|
||||
|
||||
/*! Is attribute and is either of form xmlns="", or xmlns:x="" */
|
||||
int
|
||||
isxmlns(cxobj *x)
|
||||
{
|
||||
char *prefix = NULL;
|
||||
|
||||
if (xml_type(x) != CX_ATTR)
|
||||
return 0;
|
||||
if (strcmp(xml_name(x), "xmlns")==0)
|
||||
return 1;
|
||||
if ((prefix = xml_prefix(x)) != NULL
|
||||
&& strcmp(xml_prefix(x), "xmlns")==0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*! x is element and has eactly one child which in turn has none */
|
||||
static int
|
||||
tleaf(cxobj *x)
|
||||
|
|
@ -344,27 +360,28 @@ validate_identityref(cxobj *xt,
|
|||
{
|
||||
int retval = -1;
|
||||
char *node = NULL;
|
||||
char *idref = NULL;
|
||||
yang_stmt *ybaseref; /* This is the type's base reference */
|
||||
yang_stmt *ybaseid;
|
||||
char *prefix = NULL;
|
||||
char *id = NULL;
|
||||
cbuf *cberr = NULL;
|
||||
cbuf *cb = NULL;
|
||||
cbuf *cb2 = NULL;
|
||||
|
||||
/* Get idref value. Then see if this value is derived from ytype.
|
||||
* Always add default prefix because derived identifiers are stored with
|
||||
* prefixes in the base identifiers derived-list.
|
||||
*/
|
||||
|
||||
if ((cb = cbuf_new()) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
if ((cberr = cbuf_new()) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
/* Get idref value. Then see if this value is derived from ytype.
|
||||
*/
|
||||
if ((node = xml_body(xt)) == NULL)
|
||||
return 0;
|
||||
if (strchr(node, ':') == NULL){
|
||||
prefix = yang_find_myprefix(ys);
|
||||
cprintf(cb, "%s:%s", prefix, node);
|
||||
node = cbuf_get(cb);
|
||||
}
|
||||
if (nodeid_split(node, &prefix, &id) < 0)
|
||||
goto done;
|
||||
/* This is the type's base reference */
|
||||
if ((ybaseref = yang_find(ytype, Y_BASE, NULL)) == NULL){
|
||||
if (netconf_missing_element_xml(xret, "application", yang_argument_get(ytype), "Identityref validation failed, no base") < 0)
|
||||
|
|
@ -377,26 +394,73 @@ validate_identityref(cxobj *xt,
|
|||
goto done;
|
||||
goto fail;
|
||||
}
|
||||
|
||||
#if 0 /* Assume proper namespace, otherwise we assume module prefixes */
|
||||
{
|
||||
char *namespace;
|
||||
yang_stmt *ymod;
|
||||
yang_stmt *yspec;
|
||||
|
||||
/* Create an idref as <module>:<id> which is the format of the derived
|
||||
* identityref list associated with the base identities.
|
||||
*/
|
||||
/* Get namespace (of idref) from xml */
|
||||
if (xml2ns(xt, prefix, &namespace) < 0)
|
||||
goto done;
|
||||
yspec = ys_spec(ys);
|
||||
/* Get module of that namespace */
|
||||
if ((ymod = yang_find_module_by_namespace(yspec, namespace)) == NULL){
|
||||
clicon_err(OE_YANG, ENOENT, "No module found");
|
||||
goto done;
|
||||
}
|
||||
cprintf(cb, "%s:%s", yang_argument_get(ymod), id);
|
||||
}
|
||||
#else
|
||||
#ifdef USE_IDREF_LIST_MODULE
|
||||
{
|
||||
yang_stmt *ymod;
|
||||
/* idref from prefix:id to module:id */
|
||||
if (prefix == NULL)
|
||||
ymod = ys_module(ys);
|
||||
else /* from prefix to name */
|
||||
ymod = yang_find_module_by_prefix(ys, prefix);
|
||||
if (ymod == NULL){
|
||||
cprintf(cberr, "Identityref validation failed, %s not derived from %s",
|
||||
node, yang_argument_get(ybaseid));
|
||||
if (netconf_operation_failed_xml(xret, "application", cbuf_get(cberr)) < 0)
|
||||
goto done;
|
||||
goto fail;
|
||||
}
|
||||
cprintf(cb, "%s:%s", yang_argument_get(ymod), id);
|
||||
}
|
||||
#else
|
||||
if (prefix == NULL)
|
||||
cprintf(cb, "%s:%s", yang_find_myprefix(ys), id);
|
||||
else
|
||||
cprintf(cb, "%s:%s", prefix, id);
|
||||
#endif
|
||||
#endif
|
||||
idref = cbuf_get(cb);
|
||||
/* Here check if node is in the derived node list of the base identity
|
||||
* The derived node list is a cvec computed XXX
|
||||
*/
|
||||
if (cvec_find(yang_cvec_get(ybaseid), node) == NULL){
|
||||
if ((cb2 = cbuf_new()) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
cprintf(cb2, "Identityref validation failed, %s not derived from %s",
|
||||
if (cvec_find(yang_cvec_get(ybaseid), idref) == NULL){
|
||||
cprintf(cberr, "Identityref validation failed, %s not derived from %s",
|
||||
node, yang_argument_get(ybaseid));
|
||||
if (netconf_operation_failed_xml(xret, "application", cbuf_get(cb2)) < 0)
|
||||
if (netconf_operation_failed_xml(xret, "application", cbuf_get(cberr)) < 0)
|
||||
goto done;
|
||||
goto fail;
|
||||
}
|
||||
retval = 1;
|
||||
done:
|
||||
if (cberr)
|
||||
cbuf_free(cberr);
|
||||
if (cb)
|
||||
cbuf_free(cb);
|
||||
if (cb2)
|
||||
cbuf_free(cb2);
|
||||
if (id)
|
||||
free(id);
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
return retval;
|
||||
fail:
|
||||
retval = 0;
|
||||
|
|
|
|||
|
|
@ -1102,44 +1102,6 @@ ys_spec(yang_stmt *ys)
|
|||
return (yang_stmt*)ys;
|
||||
}
|
||||
|
||||
/* Assume argument is id on the type: <[prefix:]id>, return 'id'
|
||||
* Just return string from id
|
||||
* @param[in] ys A yang statement
|
||||
* @retval NULL No id (argument is NULL)
|
||||
* @retval id Pointer to identifier
|
||||
* @see yarg_prefix
|
||||
*/
|
||||
char*
|
||||
yarg_id(yang_stmt *ys)
|
||||
{
|
||||
char *id;
|
||||
|
||||
if ((id = strchr(ys->ys_argument, ':')) == NULL)
|
||||
id = ys->ys_argument;
|
||||
else
|
||||
id++;
|
||||
return id;
|
||||
}
|
||||
|
||||
/*! Assume argument is id on the type: <[prefix:]id>, return 'prefix'
|
||||
* @param[in] ys A yang statement
|
||||
* @retval NULL No prefix
|
||||
* @retval prefix Malloced string that needs to be freed by caller.
|
||||
* @see yarg_id
|
||||
*/
|
||||
char*
|
||||
yarg_prefix(yang_stmt *ys)
|
||||
{
|
||||
char *id;
|
||||
char *prefix = NULL;
|
||||
|
||||
if ((id = strchr(ys->ys_argument, ':')) != NULL){
|
||||
prefix = strdup(ys->ys_argument);
|
||||
prefix[id-ys->ys_argument] = '\0';
|
||||
}
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/*! Given a yang statement and a prefix, return yang module to that relative prefix
|
||||
* Note, not the other module but the proxy import statement only
|
||||
* @param[in] ys A yang statement
|
||||
|
|
@ -1333,8 +1295,8 @@ yang_print_cbuf(cbuf *cb,
|
|||
* 4. Check if leaf is part of list, if key exists mark leaf as key/unique
|
||||
* XXX: extend type search
|
||||
*
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement to populate.
|
||||
* @param[in] arg A void argument not used
|
||||
* @retval 0 OK
|
||||
* @retval -1 Error with clicon_err called
|
||||
*/
|
||||
|
|
@ -1352,17 +1314,17 @@ ys_populate_leaf(clicon_handle h,
|
|||
char *reason = NULL;
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype; /* resolved type */
|
||||
char *type; /* original type */
|
||||
char *origtype=NULL; /* original type */
|
||||
uint8_t fraction_digits;
|
||||
int options = 0x0;
|
||||
|
||||
yparent = ys->ys_parent; /* Find parent: list/container */
|
||||
/* 1. Find type specification and set cv type accordingly */
|
||||
if (yang_type_get(ys, &type, &yrestype, &options, NULL, NULL, NULL, &fraction_digits)
|
||||
if (yang_type_get(ys, &origtype, &yrestype, &options, NULL, NULL, NULL, &fraction_digits)
|
||||
< 0)
|
||||
goto done;
|
||||
restype = yrestype?yrestype->ys_argument:NULL;
|
||||
if (clicon_type2cv(type, restype, ys, &cvtype) < 0) /* This handles non-resolved also */
|
||||
if (clicon_type2cv(origtype, restype, ys, &cvtype) < 0) /* This handles non-resolved also */
|
||||
goto done;
|
||||
/* 2. Create the CV using cvtype and name it */
|
||||
if ((cv = cv_new(cvtype)) == NULL){
|
||||
|
|
@ -1402,11 +1364,17 @@ ys_populate_leaf(clicon_handle h,
|
|||
ys->ys_cv = cv;
|
||||
retval = 0;
|
||||
done:
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
if (cv && retval < 0)
|
||||
cv_free(cv);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Populate list yang statement
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (type) to populate.
|
||||
*/
|
||||
static int
|
||||
ys_populate_list(clicon_handle h,
|
||||
yang_stmt *ys)
|
||||
|
|
@ -1505,6 +1473,8 @@ range_parse(yang_stmt *ys,
|
|||
/*! Populate string built-in range statement
|
||||
*
|
||||
* Create cvec variables "range_min" and "range_max". Assume parent is type.
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (range) to populate.
|
||||
* Actually: bound[..bound] (| bound[..bound])*
|
||||
* where bound is integer, decimal or keywords 'min' or 'max.
|
||||
* RFC 7950 9.2.4:
|
||||
|
|
@ -1519,10 +1489,10 @@ ys_populate_range(clicon_handle h,
|
|||
yang_stmt *ys)
|
||||
{
|
||||
int retval = -1;
|
||||
yang_stmt *yparent; /* type */
|
||||
char *origtype; /* orig type */
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype; /* resolved type */
|
||||
yang_stmt *yparent; /* type */
|
||||
char *origtype = NULL; /* orig type */
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype; /* resolved type */
|
||||
int options = 0x0;
|
||||
uint8_t fraction_digits;
|
||||
enum cv_type cvtype = CGV_ERR;
|
||||
|
|
@ -1536,7 +1506,8 @@ ys_populate_range(clicon_handle h,
|
|||
&options, NULL, NULL, NULL, &fraction_digits) < 0)
|
||||
goto done;
|
||||
restype = yrestype?yrestype->ys_argument:NULL;
|
||||
origtype = yarg_id((yang_stmt*)yparent);
|
||||
if (nodeid_split(yang_argument_get(yparent), NULL, &origtype) < 0)
|
||||
goto done;
|
||||
/* This handles non-resolved also */
|
||||
if (clicon_type2cv(origtype, restype, ys, &cvtype) < 0)
|
||||
goto done;
|
||||
|
|
@ -1548,15 +1519,21 @@ ys_populate_range(clicon_handle h,
|
|||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Populate integer built-in length statement
|
||||
*
|
||||
* Create cvec variables "range_min" and "range_max". Assume parent is type.
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (length) to populate.
|
||||
*
|
||||
* Actually: len[..len] (| len[..len])*
|
||||
* len is unsigned integer or keywords 'min' or 'max.
|
||||
* RFC 7950 9.4.4
|
||||
* len is unsigned integer or keywords 'min' or 'max'
|
||||
*
|
||||
* From RFC 7950 Sec 9.4.4:
|
||||
* A length range consists of an explicit value, or a lower bound, two
|
||||
* consecutive dots "..", and an upper bound. Multiple values or ranges
|
||||
* can be given, separated by "|". Length-restricting values MUST NOT
|
||||
|
|
@ -1586,8 +1563,8 @@ ys_populate_length(clicon_handle h,
|
|||
|
||||
/*! Sanity check yang type statement
|
||||
* XXX: Replace with generic parent/child type-check
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (type) to populate.
|
||||
* @
|
||||
*/
|
||||
static int
|
||||
ys_populate_type(clicon_handle h,
|
||||
|
|
@ -1620,12 +1597,15 @@ ys_populate_type(clicon_handle h,
|
|||
return retval;
|
||||
}
|
||||
|
||||
/*! Sanity check yang identity statement recursively
|
||||
/*! Sanity check yang identity statement recursively and create derived id list
|
||||
*
|
||||
* Find base identities if any and add this identity to derived list.
|
||||
* Find base identities if any and add this identity to derived identity list.
|
||||
* Do this recursively
|
||||
* @param[in] ys The yang identity to populate.
|
||||
* @param[in] arg If set contains a derived identifier
|
||||
* The derived identity list is a list of <module>:<id> pairs. Prefixes cannot
|
||||
* be used since they are local in scope.
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang identity to populate.
|
||||
* @param[in] idref If set contains the derived identifier(NULL on top call)
|
||||
* @see validate_identityref which in runtime validates actual values
|
||||
*/
|
||||
static int
|
||||
|
|
@ -1636,28 +1616,40 @@ ys_populate_identity(clicon_handle h,
|
|||
int retval = -1;
|
||||
yang_stmt *yc = NULL;
|
||||
yang_stmt *ybaseid;
|
||||
// yang_stmt *ymod;
|
||||
cg_var *cv;
|
||||
char *derid;
|
||||
char *baseid;
|
||||
char *prefix = NULL;
|
||||
char *id = NULL;
|
||||
cbuf *cb = NULL;
|
||||
char *p;
|
||||
|
||||
/* Top-call (no recursion) create idref
|
||||
* The idref is (here) in "canonical form": <module>:<id>
|
||||
*/
|
||||
if (idref == NULL){
|
||||
/* Create derived identity through prefix:id if not recursively called*/
|
||||
derid = ys->ys_argument; /* derived id */
|
||||
if ((prefix = yarg_prefix(ys)) == NULL){
|
||||
if ((p = yang_find_myprefix(ys)) != NULL)
|
||||
prefix = strdup(yang_find_myprefix(ys));
|
||||
}
|
||||
if ((cb = cbuf_new()) == NULL){
|
||||
clicon_err(OE_UNIX, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
if (prefix)
|
||||
cprintf(cb, "%s:%s", prefix, derid);
|
||||
else
|
||||
cprintf(cb, "%s", derid);
|
||||
#if 0 /* Use module:id instead of prefix:id in derived list */
|
||||
if (nodeid_split(yang_argument_get(ys), &prefix, &id) < 0)
|
||||
goto done;
|
||||
if ((ymod = ys_module(ys)) == NULL){
|
||||
clicon_err(OE_YANG, ENOENT, "No module found");
|
||||
goto done;
|
||||
}
|
||||
cprintf(cb, "%s:%s", yang_argument_get(ymod), id);
|
||||
#else
|
||||
{
|
||||
if (nodeid_split(yang_argument_get(ys), &prefix, &id) < 0)
|
||||
goto done;
|
||||
if (prefix)
|
||||
cprintf(cb, "%s:%s", prefix, id);
|
||||
else
|
||||
cprintf(cb, "%s:%s", yang_find_myprefix(ys), id);
|
||||
}
|
||||
#endif
|
||||
idref = cbuf_get(cb);
|
||||
}
|
||||
/* Iterate through all base statements and check the base identity exists
|
||||
|
|
@ -1667,9 +1659,9 @@ ys_populate_identity(clicon_handle h,
|
|||
while ((yc = yn_each(ys, yc)) != NULL) {
|
||||
if (yc->ys_keyword != Y_BASE)
|
||||
continue;
|
||||
baseid = yc->ys_argument;
|
||||
baseid = yang_argument_get(yc); /* on the form: prefix:id */
|
||||
if (((ybaseid = yang_find_identity(ys, baseid))) == NULL){
|
||||
clicon_err(OE_YANG, 0, "No such identity: %s", baseid);
|
||||
clicon_err(OE_YANG, ENOENT, "No such identity: %s", baseid);
|
||||
goto done;
|
||||
}
|
||||
// continue; /* root identity */
|
||||
|
|
@ -1696,6 +1688,8 @@ ys_populate_identity(clicon_handle h,
|
|||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (id)
|
||||
free(id);
|
||||
if (cb)
|
||||
cbuf_free(cb);
|
||||
return retval;
|
||||
|
|
@ -1730,8 +1724,8 @@ if_feature(yang_stmt *yspec,
|
|||
|
||||
/*! Populate yang feature statement - set cv to 1 if enabled
|
||||
*
|
||||
* @param[in] ys Feature yang statement to populate.
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys Feature yang statement to populate.
|
||||
*/
|
||||
static int
|
||||
ys_populate_feature(clicon_handle h,
|
||||
|
|
@ -1792,6 +1786,8 @@ ys_populate_feature(clicon_handle h,
|
|||
}
|
||||
|
||||
/*! Populate the unique statement with a cvec
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (unique) to populate.
|
||||
*/
|
||||
static int
|
||||
ys_populate_unique(clicon_handle h,
|
||||
|
|
@ -1805,6 +1801,8 @@ ys_populate_unique(clicon_handle h,
|
|||
}
|
||||
|
||||
/*! Populate unknown node with extension
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ys The yang statement (unknown) to populate.
|
||||
*/
|
||||
static int
|
||||
ys_populate_unknown(clicon_handle h,
|
||||
|
|
@ -1815,19 +1813,19 @@ ys_populate_unknown(clicon_handle h,
|
|||
char *reason = NULL;
|
||||
yang_stmt *ymod;
|
||||
char *prefix = NULL;
|
||||
char *name;
|
||||
char *id = NULL;
|
||||
char *extra;
|
||||
|
||||
if ((extra = ys->ys_extra) == NULL)
|
||||
goto ok;
|
||||
/* Find extension, if found, store it as unknown, if not,
|
||||
break for error */
|
||||
prefix = yarg_prefix(ys); /* And this its prefix */
|
||||
name = yarg_id(ys); /* This is the type to resolve */
|
||||
if (nodeid_split(yang_argument_get(ys), &prefix, &id) < 0)
|
||||
goto done;
|
||||
if ((ymod = yang_find_module_by_prefix(ys, prefix)) == NULL)
|
||||
goto ok; /* shouldnt happen */
|
||||
if (yang_find(ymod, Y_EXTENSION, name) == NULL){
|
||||
clicon_err(OE_YANG, errno, "Extension %s:%s not found", prefix, name);
|
||||
if (yang_find(ymod, Y_EXTENSION, id) == NULL){
|
||||
clicon_err(OE_YANG, errno, "Extension %s:%s not found", prefix, id);
|
||||
goto done;
|
||||
}
|
||||
if ((ys->ys_cv = cv_new(CGV_STRING)) == NULL){
|
||||
|
|
@ -1847,13 +1845,15 @@ ys_populate_unknown(clicon_handle h,
|
|||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (id)
|
||||
free(id);
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Populate with cligen-variables, default values, etc. Sanity checks on complete tree.
|
||||
*
|
||||
* @param[in] ys Yang statement
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] arg Argument - in effect Clicon handle
|
||||
* Preferably run this command using yang_apply
|
||||
* Done in 2nd pass after complete parsing to be sure to have a complete
|
||||
* parse-tree
|
||||
|
|
@ -2150,8 +2150,8 @@ yang_expand_grouping(yang_stmt *yn)
|
|||
int glen;
|
||||
int i;
|
||||
int j;
|
||||
char *name;
|
||||
char *prefix;
|
||||
char *id = NULL;
|
||||
char *prefix = NULL;
|
||||
size_t size;
|
||||
|
||||
/* Cannot use yang_apply here since child-list is modified (is destructive) */
|
||||
|
|
@ -2161,14 +2161,18 @@ yang_expand_grouping(yang_stmt *yn)
|
|||
switch(ys->ys_keyword){
|
||||
case Y_USES:
|
||||
/* Split argument into prefix and name */
|
||||
name = yarg_id(ys); /* This is uses/grouping name to resolve */
|
||||
prefix = yarg_prefix(ys); /* And this its prefix */
|
||||
if (ys_grouping_resolve(ys, prefix, name, &ygrouping) < 0)
|
||||
if (nodeid_split(yang_argument_get(ys), &prefix, &id) < 0)
|
||||
goto done;
|
||||
if (ys_grouping_resolve(ys, prefix, id, &ygrouping) < 0)
|
||||
goto done;
|
||||
if (prefix){
|
||||
free(prefix);
|
||||
prefix = NULL;
|
||||
}
|
||||
if (id){
|
||||
free(id);
|
||||
id = NULL;
|
||||
}
|
||||
if (ygrouping == NULL){
|
||||
clicon_log(LOG_NOTICE, "%s: Yang error : grouping \"%s\" not found in module \"%s\"",
|
||||
__FUNCTION__, ys->ys_argument, ys_module(ys)->ys_argument);
|
||||
|
|
@ -2258,6 +2262,10 @@ yang_expand_grouping(yang_stmt *yn)
|
|||
}
|
||||
retval = 0;
|
||||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (id)
|
||||
free(id);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ struct yang_stmt{
|
|||
cvec *ys_cvec; /* List of stmt-specific variables
|
||||
Y_RANGE: range_min, range_max
|
||||
Y_LIST: vector of keys
|
||||
Y_TYPE & identity: store all derived types
|
||||
Y_TYPE & identity: store all derived
|
||||
types as <prefix>:<id> list
|
||||
*/
|
||||
yang_type_cache *ys_typecache; /* If ys_keyword==Y_TYPE, cache all typedef data except unions */
|
||||
int _ys_vector_i; /* internal use: yn_each */
|
||||
|
|
|
|||
|
|
@ -1058,7 +1058,7 @@ ys_cv_validate(clicon_handle h,
|
|||
cvec *patterns = NULL;
|
||||
cvec *regexps = NULL;
|
||||
enum cv_type cvtype;
|
||||
char *type; /* orig type */
|
||||
char *origtype = NULL; /* orig type */
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype;
|
||||
uint8_t fraction = 0;
|
||||
|
|
@ -1081,13 +1081,13 @@ ys_cv_validate(clicon_handle h,
|
|||
clicon_err(OE_UNIX, errno, "cvec_new");
|
||||
goto done;
|
||||
}
|
||||
if (yang_type_get(ys, &type, &yrestype,
|
||||
if (yang_type_get(ys, &origtype, &yrestype,
|
||||
&options, &cvv,
|
||||
patterns, regexps,
|
||||
&fraction) < 0)
|
||||
goto done;
|
||||
restype = yrestype?yrestype->ys_argument:NULL;
|
||||
if (clicon_type2cv(type, restype, ys, &cvtype) < 0)
|
||||
if (clicon_type2cv(origtype, restype, ys, &cvtype) < 0)
|
||||
goto done;
|
||||
|
||||
if (cv_type_get(ycv) != cvtype){
|
||||
|
|
@ -1104,7 +1104,7 @@ ys_cv_validate(clicon_handle h,
|
|||
if (restype && strcmp(restype, "union") == 0){
|
||||
assert(cvtype == CGV_REST);
|
||||
val = cv_string_get(cv);
|
||||
if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, type, val)) < 0)
|
||||
if ((retval2 = ys_cv_validate_union(h, ys, reason, yrestype, origtype, val)) < 0)
|
||||
goto done;
|
||||
retval = retval2; /* invalid (0) with latest reason or valid 1 */
|
||||
}
|
||||
|
|
@ -1127,6 +1127,8 @@ ys_cv_validate(clicon_handle h,
|
|||
goto done;
|
||||
}
|
||||
done:
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
if (regexps)
|
||||
cvec_free(regexps);
|
||||
if (patterns)
|
||||
|
|
@ -1165,48 +1167,43 @@ ys_typedef_up(yang_stmt *ys)
|
|||
}
|
||||
|
||||
/*! Find identity yang-stmt
|
||||
This is a sanity check of base identity of identity-ref and for identity
|
||||
statements when parsing.
|
||||
|
||||
Return true if node is identityref and is derived from identity_name
|
||||
The derived-from() function returns true if the (first) node (in
|
||||
document order in the argument "nodes") is a node of type identityref,
|
||||
and its value is an identity that is derived from the identity
|
||||
"identity-name" defined in the YANG module "module-name"; otherwise
|
||||
it returns false.
|
||||
|
||||
Valid values for an identityref are any identities derived from the
|
||||
identityref's base identity.
|
||||
1. (base) identity must exist (be found). This is a sanity check
|
||||
of the specification and also necessary for identity statements.
|
||||
(This is what is done here)
|
||||
2. Check if a given node has value derived from base identity. This is
|
||||
a run-time check necessary when validating eg netconf.
|
||||
(This is validation)
|
||||
3. Find all valid derived identities from a identityref base identity.
|
||||
(This is for cli generation)
|
||||
* @param[in] ys Yang spec of id statement
|
||||
* @param[in] identity Identity string -check if it exists
|
||||
* @retval 0 OK
|
||||
* @see validate_identityref for (2) above
|
||||
* This is a sanity check of base identity of identity-ref and for identity
|
||||
* statements when parsing.
|
||||
*
|
||||
* Return true if node is identityref and is derived from identity_name
|
||||
* The derived-from() function returns true if the (first) node (in
|
||||
* document order in the argument "nodes") is a node of type identityref,
|
||||
* and its value is an identity that is derived from the identity
|
||||
* "identity-name" defined in the YANG module "module-name"; otherwise
|
||||
* it returns false.
|
||||
*
|
||||
* Valid values for an identityref are any identities derived from the
|
||||
* identityref's base identity.
|
||||
* 1. (base) identity must exist (be found). This is a sanity check
|
||||
* of the specification and also necessary for identity statements.
|
||||
* (This is what is done here)
|
||||
* 2. Check if a given node has value derived from base identity. This is
|
||||
* a run-time check necessary when validating eg netconf.
|
||||
* (This is validation)
|
||||
* 3. Find all valid derived identities from a identityref base identity.
|
||||
* (This is for cli generation)
|
||||
* @param[in] ys Yang spec of id statement
|
||||
* @param[in] identity Identity string -check if it exists
|
||||
* @retval 0 OK
|
||||
* @see validate_identityref for (2) above
|
||||
*/
|
||||
yang_stmt *
|
||||
yang_find_identity(yang_stmt *ys,
|
||||
char *identity)
|
||||
{
|
||||
char *id;
|
||||
char *id = NULL;
|
||||
char *prefix = NULL;
|
||||
yang_stmt *ymodule;
|
||||
yang_stmt *yid = NULL;
|
||||
yang_stmt *yn;
|
||||
|
||||
if ((id = strchr(identity, ':')) == NULL)
|
||||
id = identity;
|
||||
else{
|
||||
prefix = strdup(identity);
|
||||
prefix[id-identity] = '\0';
|
||||
id++;
|
||||
}
|
||||
if (nodeid_split(identity, &prefix, &id) < 0)
|
||||
goto done;
|
||||
/* No, now check if identityref is derived from base */
|
||||
if (prefix){ /* Go to top and find import that matches */
|
||||
if ((ymodule = yang_find_module_by_prefix(ys, prefix)) == NULL)
|
||||
|
|
@ -1231,6 +1228,8 @@ yang_find_identity(yang_stmt *ys,
|
|||
}
|
||||
}
|
||||
done:
|
||||
if (id)
|
||||
free(id);
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
return yid;
|
||||
|
|
@ -1334,7 +1333,7 @@ yang_type_resolve(yang_stmt *yorig,
|
|||
{
|
||||
yang_stmt *rytypedef = NULL; /* Resolved typedef of ytype */
|
||||
yang_stmt *rytype; /* Resolved type of ytype */
|
||||
char *type;
|
||||
char *type = NULL;
|
||||
char *prefix = NULL;
|
||||
int retval = -1;
|
||||
yang_stmt *yn;
|
||||
|
|
@ -1343,8 +1342,9 @@ yang_type_resolve(yang_stmt *yorig,
|
|||
if (options)
|
||||
*options = 0x0;
|
||||
*yrestype = NULL; /* Initialization of resolved type that may not be necessary */
|
||||
type = yarg_id(ytype); /* This is the type to resolve */
|
||||
prefix = yarg_prefix(ytype); /* And this its prefix */
|
||||
|
||||
if (nodeid_split(yang_argument_get(ytype), &prefix, &type) < 0)
|
||||
goto done;
|
||||
/* Cache does not work for eg string length 32? */
|
||||
if (ytype->ys_typecache != NULL){
|
||||
if (yang_type_cache_get(ytype->ys_typecache, yrestype,
|
||||
|
|
@ -1409,6 +1409,8 @@ yang_type_resolve(yang_stmt *yorig,
|
|||
done:
|
||||
if (prefix)
|
||||
free(prefix);
|
||||
if (type)
|
||||
free(type);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
@ -1432,7 +1434,7 @@ yang_type_resolve(yang_stmt *yorig,
|
|||
* printf("%d..%d\n", min , max);
|
||||
* @endcode
|
||||
* @param[in] ys yang-stmt, leaf or leaf-list
|
||||
* @param[out] origtype original type may be derived or built-in
|
||||
* @param[out] origtype original type may be derived or built-in (malloced)
|
||||
* @param[out] yrestype Resolved type. return built-in type or NULL.
|
||||
* @param[out] options Flags field of optional values, see YANG_OPTIONS_*
|
||||
* @param[out] cvv Cvec with min/max range or length.
|
||||
|
|
@ -1465,19 +1467,23 @@ yang_type_get(yang_stmt *ys,
|
|||
{
|
||||
int retval = -1;
|
||||
yang_stmt *ytype; /* type */
|
||||
char *type;
|
||||
char *type = NULL;
|
||||
|
||||
if (options)
|
||||
*options = 0x0;
|
||||
/* Find mandatory type */
|
||||
if ((ytype = yang_find(ys, Y_TYPE, NULL)) == NULL){
|
||||
clicon_err(OE_DB, 0, "mandatory type object is not found");
|
||||
clicon_err(OE_DB, ENOENT, "mandatory type object is not found");
|
||||
goto done;
|
||||
}
|
||||
/* XXX: here we seem to have some problems if type is union */
|
||||
type = yarg_id(ytype);
|
||||
if (origtype)
|
||||
*origtype = type;
|
||||
if (nodeid_split(yang_argument_get(ytype), NULL, &type) < 0)
|
||||
goto done;
|
||||
if (origtype &&
|
||||
(*origtype = strdup(type)) == NULL){
|
||||
clicon_err(OE_XML, errno, "stdup");
|
||||
goto done;
|
||||
}
|
||||
if (yang_type_resolve(ys, ys, ytype, yrestype,
|
||||
options, cvv, patterns, regexps, fraction) < 0)
|
||||
goto done;
|
||||
|
|
@ -1485,6 +1491,8 @@ yang_type_get(yang_stmt *ys,
|
|||
*yrestype?(*yrestype)->ys_argument:"null");
|
||||
retval = 0;
|
||||
done:
|
||||
if (type)
|
||||
free(type);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
@ -1496,16 +1504,18 @@ yang_type2cv(yang_stmt *ys)
|
|||
{
|
||||
yang_stmt *yrestype; /* resolved type */
|
||||
char *restype; /* resolved type */
|
||||
char *type; /* original type */
|
||||
char *origtype=NULL; /* original type */
|
||||
enum cv_type cvtype = CGV_ERR;
|
||||
|
||||
/* Find type specification */
|
||||
if (yang_type_get(ys, &type, &yrestype, NULL, NULL, NULL, NULL, NULL)
|
||||
if (yang_type_get(ys, &origtype, &yrestype, NULL, NULL, NULL, NULL, NULL)
|
||||
< 0)
|
||||
goto done;
|
||||
restype = yrestype?yrestype->ys_argument:NULL;
|
||||
if (clicon_type2cv(type, restype, ys, &cvtype) < 0) /* This handles non-resolved also */
|
||||
if (clicon_type2cv(origtype, restype, ys, &cvtype) < 0) /* This handles non-resolved also */
|
||||
goto done;
|
||||
done:
|
||||
if (origtype)
|
||||
free(origtype);
|
||||
return cvtype;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ new "Netconf set undefined acl-type"
|
|||
expecteof "$clixon_netconf -qf $cfg" 0 '<rpc><edit-config><target><candidate/></target><config><acls xmlns="urn:example:my-crypto"><acl><name>x</name><type>undefined</type></acl></acls></config></edit-config></rpc>]]>]]>' '^<rpc-reply><ok/></rpc-reply>]]>]]>$'
|
||||
|
||||
new "netconf validate fail"
|
||||
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><validate><source><candidate/></source></validate></rpc>]]>]]>" '^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>operation-failed</error-tag><error-severity>error</error-severity><error-message>Identityref validation failed, mc:undefined not derived from acl-base</error-message></rpc-error></rpc-reply>]]>]]>'
|
||||
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><validate><source><candidate/></source></validate></rpc>]]>]]>" '^<rpc-reply><rpc-error><error-type>application</error-type><error-tag>operation-failed</error-tag><error-severity>error</error-severity><error-message>Identityref validation failed, undefined not derived from acl-base</error-message></rpc-error></rpc-reply>]]>]]>'
|
||||
|
||||
new "netconf discard-changes"
|
||||
expecteof "$clixon_netconf -qf $cfg" 0 "<rpc><discard-changes/></rpc>]]>]]>" "^<rpc-reply><ok/></rpc-reply>]]>]]>$"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue