clixon/datastore/keyvalue/clixon_keyvalue.c
2017-04-08 20:39:04 +02:00

1746 lines
44 KiB
C

/*
*
***** BEGIN LICENSE BLOCK *****
Copyright (C) 2009-2017 Olof Hagsand and Benny Holmgren
This file is part of CLIXON.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Alternatively, the contents of this file may be used under the terms of
the GNU General Public License Version 3 or later (the "GPL"),
in which case the provisions of the GPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of the GPL, and not to allow others to
use your version of this file under the terms of Apache License version 2,
indicate your decision by deleting the provisions above and replace them with
the notice and other provisions required by the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the Apache License version 2 or the GPL.
***** END LICENSE BLOCK *****
*/
/*
* An xml database consists of key-value pairs for xml-trees.
* Each node in an xml-tree has a key and an optional value.
* The key (xmlkey) is constructed from the xml node name concatenated
* with its ancestors and any eventual list keys.
* A xmlkeyfmt is a help-structure used when accessing the XML database.
* It consists of an xmlkey but with the key fields replaced with wild-chars(%s)
* Example: /aaa/bbb/%s/%s/ccc
* Such an xmlkeyfmt can be obtained from a yang-statement by following
* its ancestors to the root module. If one of the ancestors is a list,
* a wildchar (%s) is inserted for each key.
* These xmlkeyfmt keys are saved and used in cli callbacks such as when
* modifying syntax (eg cli_merge/cli_delete) or when completing for sub-symbols
* In this case, the variables are set and the wildcards can be instantiated.
* An xml tree can then be formed that can be used to the xmldb_get() or
* xmldb_put() functions.
* The relations between the functions and formats are as follows:
*
* +-----------------+ +-----------------+
* | yang-stmt | yang2xmlkeyfmt | xmlkeyfmt | xmlkeyfmt2xpath
* | list aa,leaf k | ----------------->| /aa=%s |---------------->
* +-----------------+ +-----------------+
* |
* | xmlkeyfmt2key
* | k=17
* v
* +-------------------+ +-----------------+
* | xml-tree/cxobj | xmlkey2xml | xmlkey RFC3986|
* | <aa><k>17</k></aa>| <------------- | /aa=17 |
* +-------------------+ +-----------------+
*
* Alternative for xmlkeyfmt would be eg:
* RESTCONF: /interfaces/interface=%s/ipv4/address/ip=%s (used)
* XPATH: /interfaces/interface[name=%s]/ipv4/address/[ip=%s]
*
* Paths through the code (for coverage)
* cli_callback_generate +----------------+
* cli_expand_var_generate | yang2xmlkeyfmt |
* yang -------------> | |
* +----------------+
* xmldb_get_tree
* - compare_dbs
* - netconf
* - validate
* - from_client_save
*
* xmldb_get_vec
* - restconf
* - expand_dbvar
* - show_conf_xpath
*/
#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
#endif
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <limits.h>
#include <fnmatch.h>
#include <stdint.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>
#include <assert.h>
#include <syslog.h>
#include <curl/curl.h>
/* cligen */
#include <cligen/cligen.h>
/* clicon */
#include <clixon/clixon.h>
#include "clixon_qdb.h"
#include "clixon_keyvalue.h"
/*! Database locking for candidate and running non-persistent
* Store an integer for running and candidate containing
* the session-id of the client holding the lock.
*/
static int _running_locked = 0;
static int _candidate_locked = 0;
static int _startup_locked = 0;
/*! Lock database
* @param[in] db Database name
* @param[in] pid process/session-id
*/
static int
db_lock(char *db,
int pid)
{
if (strcmp("running", db) == 0)
_running_locked = pid;
else if (strcmp("candidate", db) == 0)
_candidate_locked = pid;
else if (strcmp("startup", db) == 0)
_startup_locked = pid;
clicon_debug(1, "%s: locked by %u", db, pid);
return 0;
}
/*! Unlock database
* @param[in] db Database name
*/
static int
db_unlock(char *db)
{
if (strcmp("running", db) == 0)
_running_locked = 0;
else if (strcmp("candidate", db) == 0)
_candidate_locked = 0;
else if (strcmp("startup", db) == 0)
_startup_locked = 0;
return 0;
}
/*! Unlock all databases locked by pid (eg process dies)
* @param[in] pid process/session-id
*/
static int
db_unlock_all(int pid)
{
if (_running_locked == pid)
_running_locked = 0;
if (_candidate_locked == pid)
_candidate_locked = 0;
if (_startup_locked == pid)
_startup_locked = 0;
return 0;
}
/*! returns id of locker
* @retval 0 Not locked
* @retval >0 Id of locker
*/
static int
db_islocked(char *db)
{
if (strcmp("running", db) == 0)
return (_running_locked);
else if (strcmp("candidate", db) == 0)
return(_candidate_locked);
else if (strcmp("startup", db) == 0)
return(_startup_locked);
return 0;
}
/*! Translate from symbolic database name to actual filename in file-system
* @param[in] h Clicon handle
* @param[in] db Symbolic database name, eg "candidate", "running"
* @param[out] filename Filename. Unallocate after use with free()
* @retval 0 OK
* @retval -1 Error
* @note Could need a way to extend which databases exists, eg to register new.
* The currently allowed databases are:
* candidate, tmp, running, result
* The filename reside in CLICON_XMLDB_DIR option
*/
static int
db2file(clicon_handle h,
char *db,
char **filename)
{
int retval = -1;
cbuf *cb;
char *dir;
if ((cb = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
if ((dir = clicon_xmldb_dir(h)) == NULL){
clicon_err(OE_XML, errno, "CLICON_XMLDB_DIR not set");
goto done;
}
if (strcmp(db, "running") != 0 &&
strcmp(db, "candidate") != 0 &&
strcmp(db, "startup") != 0 &&
strcmp(db, "tmp") != 0){
clicon_err(OE_XML, 0, "Unexpected database: %s", db);
goto done;
}
cprintf(cb, "%s/%s_db", dir, db);
if ((*filename = strdup4(cbuf_get(cb))) == NULL){
clicon_err(OE_UNIX, errno, "strdup");
goto done;
}
retval = 0;
done:
if (cb)
cbuf_free(cb);
return retval;
}
/*! Help function to append key values from an xml list to a cbuf
* Example, a yang node x with keys a and b results in "x/a/b"
*/
static int
append_listkeys(cbuf *ckey,
cxobj *xt,
yang_stmt *ys)
{
int retval = -1;
yang_stmt *ykey;
cxobj *xkey;
cg_var *cvi;
cvec *cvk = NULL; /* vector of index keys */
char *keyname;
char *bodyenc;
int i=0;
if ((ykey = yang_find((yang_node*)ys, Y_KEY, NULL)) == NULL){
clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key",
__FUNCTION__, ys->ys_argument);
goto done;
}
/* The value is a list of keys: <key>[ <key>]* */
if ((cvk = yang_arg2cvec(ykey, " ")) == NULL)
goto done;
cvi = NULL;
/* Iterate over individual keys */
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
keyname = cv_string_get(cvi);
if ((xkey = xml_find(xt, keyname)) == NULL){
clicon_err(OE_XML, errno, "XML list node \"%s\" does not have key \"%s\" child",
xml_name(xt), keyname);
goto done;
}
if ((bodyenc = curl_easy_escape(NULL, xml_body(xkey), 0)) == NULL){
clicon_err(OE_UNIX, errno, "curl_easy_escape");
goto done;
}
if (i++)
cprintf(ckey, ",");
else
cprintf(ckey, "=");
cprintf(ckey, "%s", bodyenc);
free(bodyenc); bodyenc = NULL;
}
retval = 0;
done:
if (cvk)
cvec_free(cvk);
return retval;
}
/*! Help function to create xml key values
* @param[in,out] x Parent
* @param[in] ykey
* @param[in] arg
* @param[in] keyname yang key name
*/
static int
create_keyvalues(cxobj *x,
yang_stmt *ykey,
char *arg,
char *keyname)
{
int retval = -1;
cxobj *xb;
/* Check if key node exists */
if ((xb = xml_new_spec(keyname, x, ykey)) == NULL)
goto done;
if ((xb = xml_new("body", xb)) == NULL)
goto done;
xml_type_set(xb, CX_BODY);
xml_value_set(xb, arg);
retval = 0;
done:
return retval;
}
/*! Prune everything that has not been marked
* @param[in] xt XML tree with some node marked
* @param[out] upmark Set if a child (recursively) has marked set.
* The function removes all branches that does not contain a marked child
* XXX: maybe key leafs should not be purged if list is not purged?
* XXX: consider move to clicon_xml
*/
static int
xml_tree_prune_unmarked(cxobj *xt,
int *upmark)
{
int retval = -1;
int submark;
int mark;
cxobj *x;
cxobj *xprev;
mark = 0;
x = NULL;
xprev = x = NULL;
while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL) {
if (xml_flag(x, XML_FLAG_MARK)){
mark++;
xprev = x;
continue; /* mark and stop here */
}
if (xml_tree_prune_unmarked(x, &submark) < 0)
goto done;
if (submark)
mark++;
else{ /* Safe with xml_child_each if last */
if (xml_purge(x) < 0)
goto done;
x = xprev;
}
xprev = x;
}
retval = 0;
done:
if (upmark)
*upmark = mark;
return retval;
}
/*!
* @param[in] xk xmlkey
* @param[out] xt XML tree as result
* XXX cannot handle top-level list
*/
static int
get(char *dbname,
yang_spec *ys,
char *xk,
char *val,
cxobj *xt)
{
int retval = -1;
char **vec = NULL;
int nvec;
char **valvec = NULL;
int nvalvec;
int i;
int j;
char *name;
char *restval;
yang_stmt *y;
cxobj *x;
cxobj *xc;
cxobj *xb;
yang_stmt *ykey;
cg_var *cvi;
cvec *cvk = NULL; /* vector of index keys */
char *keyname;
char *arg;
char *argdec;
cbuf *cb;
// clicon_debug(1, "%s xkey:%s val:%s", __FUNCTION__, xk, val);
x = xt;
if (xk == NULL || *xk!='/'){
clicon_err(OE_DB, 0, "Invalid key: %s", xk);
goto done;
}
if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL)
goto done;
/* Element 0 is NULL '/',
Element 1 is top symbol and needs to find subs in all modules:
spec->module->syntaxnode
*/
if (nvec < 2){
clicon_err(OE_XML, 0, "Malformed key: %s", xk);
goto done;
}
i = 1;
while (i<nvec){
name = vec[i]; /* E.g "x=1,2" -> name:x restval=1,2 */
if ((restval = index(name, '=')) != NULL){
*restval = '\0';
restval++;
}
if (i == 1){ /* spec->module->node */
if ((y = yang_find_topnode(ys, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
goto done;
}
}
else
if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
goto done;
}
switch (y->ys_keyword){
case Y_LEAF_LIST:
/*
* If xml element is a leaf-list, then the next element is expected to
* be a value
*/
if ((argdec = curl_easy_unescape(NULL, restval, 0, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "curl_easy_escape");
goto done;
}
if ((xc = xml_find(x, name))==NULL ||
(xb = xml_find(xc, argdec))==NULL){
if ((xc = xml_new_spec(name, x, y)) == NULL)
goto done;
/* Assume body is created at end of function */
}
free(argdec);
argdec = NULL;
break;
case Y_LIST:
/*
* If xml element is a list, then the next element(s) is expected to be
* a key value. Check if this key value is already in the xml tree,
* otherwise create it.
*/
if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){
clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key",
__FUNCTION__, y->ys_argument);
goto done;
}
/* The value is a list of keys: <key>[ <key>]* */
if ((cvk = yang_arg2cvec(ykey, " ")) == NULL)
goto done;
if ((cb = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
cvi = NULL;
/* Iterate over individual yang keys */
cprintf(cb, "%s", name);
if (valvec)
free(valvec);
if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL)
goto done;
if (cvec_len(cvk)!=nvalvec){
retval = 0;
goto done;
}
j = 0;
while ((cvi = cvec_each(cvk, cvi)) != NULL){
if (j>=nvalvec)
break;
arg = valvec[j++];
if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "curl_easy_escape");
goto done;
}
cprintf(cb, "[%s=%s]", cv_string_get(cvi), argdec);
free(argdec);
argdec=NULL;
}
if ((xc = xpath_first(x, cbuf_get(cb))) == NULL){
if ((xc = xml_new_spec(name, x, y)) == NULL)
goto done;
cvi = NULL;
// i -= cvec_len(cvk);
/* Iterate over individual yang keys */
j=0;
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
if (j>=nvalvec)
break;
arg = valvec[j++];
keyname = cv_string_get(cvi);
if ((argdec = curl_easy_unescape(NULL, arg, 0, NULL)) == NULL){
clicon_err(OE_UNIX, errno, "curl_easy_escape");
goto done;
}
if (create_keyvalues(xc,
ykey,
argdec,
keyname) < 0)
goto done;
free(argdec); argdec = NULL;
} /* while */
}
if (cb){
cbuf_free(cb);
cb = NULL;
}
if (cvk){
cvec_free(cvk);
cvk = NULL;
}
break;
case Y_LEAF:
case Y_CONTAINER:
default:
if ((xc = xml_find(x, name))==NULL)
if ((xc = xml_new_spec(name, x, y)) == NULL)
goto done;
break;
} /* switch */
x = xc;
i++;
}
if (val && xml_body(x)==NULL){
if ((x = xml_new("body", x)) == NULL)
goto done;
xml_type_set(x, CX_BODY);
xml_value_set(x, val);
}
if(debug>1){
fprintf(stderr, "%s %s\n", __FUNCTION__, xk);
clicon_xml2file(stderr, xt, 0, 1);
}
retval = 0;
done:
if (vec)
free(vec);
if (valvec)
free(valvec);
if (cvk)
cvec_free(cvk);
return retval;
}
/*! Sanitize an xml tree: xml node has matching yang_stmt pointer
*/
static int
xml_sanity(cxobj *x,
void *arg)
{
int retval = -1;
yang_stmt *ys;
ys = (yang_stmt*)xml_spec(x);
if (ys==NULL){
clicon_err(OE_XML, 0, "No spec for xml node %s", xml_name(x));
goto done;
}
if (strstr(ys->ys_argument, xml_name(x))==NULL){
clicon_err(OE_XML, 0, "xml node name '%s' does not match yang spec arg '%s'",
xml_name(x), ys->ys_argument);
goto done;
}
retval = 0;
done:
return retval;
}
/*! Add default values (if not set)
*/
static int
xml_default(cxobj *x,
void *arg)
{
int retval = -1;
yang_stmt *ys;
yang_stmt *y;
int i;
cxobj *xc;
cxobj *xb;
char *str;
ys = (yang_stmt*)xml_spec(x);
/* Check leaf defaults */
if (ys->ys_keyword == Y_CONTAINER || ys->ys_keyword == Y_LIST){
for (i=0; i<ys->ys_len; i++){
y = ys->ys_stmt[i];
if (y->ys_keyword != Y_LEAF)
continue;
assert(y->ys_cv);
if (!cv_flag(y->ys_cv, V_UNSET)){ /* Default value exists */
if (!xml_find(x, y->ys_argument)){
if ((xc = xml_new_spec(y->ys_argument, x, y)) == NULL)
goto done;
if ((xb = xml_new("body", xc)) == NULL)
goto done;
xml_type_set(xb, CX_BODY);
if ((str = cv2str_dup(y->ys_cv)) == NULL){
clicon_err(OE_UNIX, errno, "cv2str_dup");
goto done;
}
if (xml_value_set(xb, str) < 0)
goto done;
free(str);
}
}
}
}
retval = 0;
done:
return retval;
}
/*! Order XML children according to YANG
*/
static int
xml_order(cxobj *x,
void *arg)
{
int retval = -1;
yang_stmt *y;
yang_stmt *yc;
int i;
int j0;
int j;
cxobj *xc;
cxobj *xj;
char *yname; /* yang child name */
char *xname; /* xml child name */
y = (yang_stmt*)xml_spec(x);
j0 = 0;
/* Go through xml children and ensure they are same order as yspec children */
for (i=0; i<y->ys_len; i++){
yc = y->ys_stmt[i];
if (!yang_is_syntax(yc))
continue;
yname = yc->ys_argument;
/* First go thru xml children with same name */
for (;j0<xml_child_nr(x); j0++){
xc = xml_child_i(x, j0);
if (xml_type(xc) != CX_ELMNT)
continue;
xname = xml_name(xc);
if (strcmp(xname, yname))
break;
}
/* Now we have children not with same name */
for (j=j0; j<xml_child_nr(x); j++){
xc = xml_child_i(x, j);
if (xml_type(xc) != CX_ELMNT)
continue;
xname = xml_name(xc);
if (strcmp(xname, yname))
continue;
/* reorder */
xj = xml_child_i(x, j0);
xml_child_i_set(x, j0, xc);
xml_child_i_set(x, j, xj);
j0++;
}
}
retval = 0;
// done:
return retval;
}
/*! Get content of database using xpath. return a set of matching sub-trees
* The function returns a minimal tree that includes all sub-trees that match
* xpath.
* @param[in] dbname Name of database to search in (filename including dir path
* @param[in] xpath String with XPATH syntax. or NULL for all
* @param[in] yspec Yang specification
* @param[out] xtop Single XML tree which xvec points to. Free with xml_free()
* @param[out] xvec Vector of xml trees. Free after use.
* @param[out] xlen Length of vector.
* @retval 0 OK
* @retval -1 Error
* @code
* cxobj *xt;
* cxobj **xvec;
* size_t xlen;
* yang_spec *yspec = clicon_dbspec_yang(h);
* if (xmldb_get("running", "/interfaces/interface[name="eth"]",
* &xt, &xvec, &xlen) < 0)
* err;
* for (i=0; i<xlen; i++){
* xn = xv[i];
* ...
* }
* xml_free(xt);
* free(xvec);
* @endcode
* @note if xvec is given, then purge tree, if not return whole tree.
* @see xpath_vec
* @see xmldb_get
*/
int
kv_get(clicon_handle h,
char *db,
char *xpath,
cxobj **xtop,
cxobj ***xvec0,
size_t *xlen0)
{
int retval = -1;
yang_spec *yspec;
char *dbname = NULL;
cxobj **xvec;
size_t xlen;
int i;
int npairs;
struct db_pair *pairs;
cxobj *xt = NULL;
clicon_debug(2, "%s", __FUNCTION__);
if (db2file(h, db, &dbname) < 0)
goto done;
if (dbname==NULL){
clicon_err(OE_XML, 0, "dbname NULL");
goto done;
}
if ((yspec = clicon_dbspec_yang(h)) == NULL){
clicon_err(OE_YANG, ENOENT, "No yang spec");
goto done;
}
/* Read in complete database (this can be optimized) */
if ((npairs = db_regexp(dbname, "", __FUNCTION__, &pairs, 0)) < 0)
goto done;
if ((xt = xml_new_spec("clicon", NULL, yspec)) == NULL)
goto done;
/* Translate to complete xml tree */
for (i = 0; i < npairs; i++) {
if (get(dbname,
yspec,
pairs[i].dp_key, /* xml key */
pairs[i].dp_val, /* may be NULL */
xt) < 0)
goto done;
}
if (xpath_vec(xt, xpath?xpath:"/", &xvec, &xlen) < 0)
goto done;
/* If vectors are specified then filter out everything else,
* otherwise return complete tree.
*/
if (xvec != NULL){
for (i=0; i<xlen; i++)
xml_flag_set(xvec[i], XML_FLAG_MARK);
}
/* Top is special case */
if (!xml_flag(xt, XML_FLAG_MARK))
if (xml_tree_prune_unmarked(xt, NULL) < 0)
goto done;
if (xml_apply(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset, (void*)XML_FLAG_MARK) < 0)
goto done;
if (xvec0 && xlen0){
*xvec0 = xvec;
xvec = NULL;
*xlen0 = xlen;
xlen = 0;
}
if (xml_apply(xt, CX_ELMNT, xml_default, NULL) < 0)
goto done;
/* XXX does not work for top-level */
if (xml_apply(xt, CX_ELMNT, xml_order, NULL) < 0)
goto done;
if (xml_apply(xt, CX_ELMNT, xml_sanity, NULL) < 0)
goto done;
if (debug>1)
clicon_xml2file(stderr, xt, 0, 1);
*xtop = xt;
retval = 0;
done:
if (dbname)
free(dbname);
if (xvec)
free(xvec);
unchunk_group(__FUNCTION__);
return retval;
}
/*! Add data to database internal recursive function
* @param[in] dbname Name of database to search in (filename incl dir path)
* @param[in] xt xml-node.
* @param[in] ys Yang statement corresponding to xml-node
* @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc
* @param[in] xkey0 aggregated xmlkey
* @retval 0 OK
* @retval -1 Error
* @note XXX op only supports merge
*/
static int
put(char *dbname,
cxobj *xt,
yang_stmt *ys,
enum operation_type op,
const char *xk0)
{
int retval = -1;
cxobj *x = NULL;
char *xk;
cbuf *cbxk = NULL;
char *body;
yang_stmt *y;
int exists;
char *bodyenc=NULL;
char *opstr;
clicon_debug(1, "%s xk0:%s ys:%s", __FUNCTION__, xk0, ys->ys_argument);
if (debug){
xml_print(stderr, xt);
// yang_print(stderr, (yang_node*)ys, 0);
}
if ((opstr = xml_find_value(xt, "operation")) != NULL)
if (xml_operation(opstr, &op) < 0)
goto done;
body = xml_body(xt);
if ((cbxk = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
cprintf(cbxk, "%s/%s", xk0, xml_name(xt));
switch (ys->ys_keyword){
case Y_LIST: /* Note: can have many keys */
if (append_listkeys(cbxk, xt, ys) < 0)
goto done;
break;
case Y_LEAF_LIST:
if ((bodyenc = curl_easy_escape(NULL, body, 0)) == NULL){
clicon_err(OE_UNIX, errno, "curl_easy_escape");
goto done;
}
cprintf(cbxk, "=%s", bodyenc);
break;
default:
break;
}
xk = cbuf_get(cbxk);
// fprintf(stderr, "%s %s\n", key, body?body:"");
/* Write to database, key and a vector of variables */
switch (op){
case OP_CREATE:
if ((exists = db_exists(dbname, xk)) < 0)
goto done;
if (exists == 1){
clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk);
goto done;
}
case OP_MERGE:
case OP_REPLACE:
if (db_set(dbname, xk, body?body:NULL, body?strlen(body)+1:0) < 0)
goto done;
break;
case OP_DELETE:
if ((exists = db_exists(dbname, xk)) < 0)
goto done;
if (exists == 0){
clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk);
goto done;
}
case OP_REMOVE:
if (db_del(dbname, xk) < 0)
goto done;
break;
case OP_NONE:
break;
}
/* For every node, create a key with values */
while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){
if ((y = yang_find_syntax((yang_node*)ys, xml_name(x))) == NULL){
clicon_err(OE_UNIX, 0, "No yang node found: %s", xml_name(x));
goto done;
}
if (put(dbname, x, y, op, xk) < 0)
goto done;
}
retval = 0;
done:
if (cbxk)
cbuf_free(cbxk);
if (bodyenc)
free(bodyenc);
return retval;
}
/*! Modify database provided an XML database key and an operation
* @param[in] h CLICON handle
* @param[in] db Database name
* @param[in] op OP_MERGE, OP_REPLACE, OP_REMOVE, etc
* @param[in] xk XML Key, eg /aa/bb=17/name
* @param[in] val Key value, eg "17"
* @retval 0 OK
* @retval -1 Error
* @code
* if (xmldb_put_xkey(h, db, OP_MERGE, "/aa/bb=17/name", "17") < 0)
* err;
* @endcode
* @see xmldb_put with xml-tree, no path
*/
static int
xmldb_put_xkey(clicon_handle h,
char *db,
enum operation_type op,
char *xk,
char *val)
{
int retval = -1;
cxobj *x = NULL;
yang_stmt *y = NULL;
yang_stmt *ykey;
char **vec = NULL;
int nvec;
char **valvec = NULL;
int nvalvec;
int i;
int j;
char *name;
char *restval;
cg_var *cvi;
cvec *cvk = NULL; /* vector of index keys */
char *val2 = NULL;
cbuf *ckey=NULL; /* partial keys */
cbuf *csubkey=NULL; /* partial keys */
cbuf *crx=NULL; /* partial keys */
char *keyname;
int exists;
int npairs;
struct db_pair *pairs;
yang_spec *yspec;
char *filename = NULL;
yspec = clicon_dbspec_yang(h);
if (db2file(h, db, &filename) < 0)
goto done;
if (xk == NULL || *xk!='/'){
clicon_err(OE_DB, 0, "Invalid key: %s", xk);
goto done;
}
if ((ckey = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if ((csubkey = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if ((vec = clicon_strsep(xk, "/", &nvec)) == NULL)
goto done;
if (nvec < 2){
clicon_err(OE_XML, 0, "Malformed key: %s", xk);
goto done;
}
i = 1;
while (i<nvec){
name = vec[i]; /* E.g "x=1,2" -> name:x restval=1,2 */
if ((restval = index(name, '=')) != NULL){
*restval = '\0';
restval++;
}
if (i==1){
if (strlen(name)==0 && (op==OP_DELETE || op == OP_REMOVE)){
/* Special handling of "/" */
cprintf(ckey, "/");
break;
}
else
if ((y = yang_find_topnode(yspec, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", x?xml_name(x):"");
goto done;
}
}
else
if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
goto done;
}
if ((op==OP_DELETE || op == OP_REMOVE) &&
y->ys_keyword == Y_LEAF &&
y->ys_parent->yn_keyword == Y_LIST &&
yang_key_match(y->ys_parent, y->ys_argument))
/* Special rule if key, dont write last key-name, rm whole*/;
else
cprintf(ckey, "/%s", name);
i++;
switch (y->ys_keyword){
case Y_LEAF_LIST:
if (restval==NULL){
clicon_err(OE_XML, 0, "malformed key, expected '=<restval>'");
goto done;
}
cprintf(ckey, "=%s", restval);
break;
case Y_LIST:
if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){
clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key",
__FUNCTION__, y->ys_argument);
goto done;
}
/* The value is a list of keys: <key>[ <key>]* */
if ((cvk = yang_arg2cvec(ykey, " ")) == NULL)
goto done;
if (restval==NULL){
clicon_err(OE_XML, 0, "malformed key, expected '=<restval>'");
goto done;
}
if (valvec)
free(valvec);
if ((valvec = clicon_strsep(restval, ",", &nvalvec)) == NULL)
goto done;
if (cvec_len(cvk) != nvalvec){
clicon_err(OE_XML, errno, "List %s key length mismatch", name);
goto done;
}
cvi = NULL;
/* Iterate over individual yang keys */
j = 0;
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
keyname = cv_string_get(cvi);
if (j)
cprintf(ckey, ",");
else
cprintf(ckey, "=");
val2 = valvec[j++];
cprintf(ckey, "%s", val2);
cbuf_reset(csubkey);
cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname);
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0)
goto done;
}
if (cvk){
cvec_free(cvk);
cvk = NULL;
}
break;
default:
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0)
goto done;
break;
}
}
xk = cbuf_get(ckey);
/* final key */
switch (op){
case OP_CREATE:
if ((exists = db_exists(filename, xk)) < 0)
goto done;
if (exists == 1){
clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", xk);
goto done;
}
case OP_MERGE:
case OP_REPLACE:
if (y->ys_keyword == Y_LEAF || y->ys_keyword == Y_LEAF_LIST){
if (db_set(filename, xk, val, val?strlen(val)+1:0) < 0)
goto done;
}
else
if (db_set(filename, xk, NULL, 0) < 0)
goto done;
break;
case OP_DELETE:
if ((exists = db_exists(filename, xk)) < 0)
goto done;
if (exists == 0){
clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", xk);
goto done;
}
case OP_REMOVE:
/* Read in complete database (this can be optimized) */
if ((crx = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
cprintf(crx, "^%s.*$", xk);
if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0)
goto done;
for (i = 0; i < npairs; i++) {
if (db_del(filename, pairs[i].dp_key) < 0)
goto done;
}
break;
default:
break;
}
retval = 0;
done:
if (filename)
free(filename);
if (ckey)
cbuf_free(ckey);
if (csubkey)
cbuf_free(csubkey);
if (crx)
cbuf_free(crx);
if (cvk)
cvec_free(cvk);
if (vec)
free(vec);
if (valvec)
free(valvec);
unchunk_group(__FUNCTION__);
return retval;
}
/*! Modify database provided an xml tree, a restconf api_path and an operation
*
* @param[in] h CLICON handle
* @param[in] db running or candidate
* @param[in] op OP_MERGE: just add it.
* OP_REPLACE: first delete whole database
* OP_NONE: operation attribute in xml determines operation
* @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13])
* @param[in] xt xml-tree. Top-level symbol is dummy
* @retval 0 OK
* @retval -1 Error
* example:
* container top {
* list list1 {
* key "key1 key2 key3";
* is referenced as
* /restconf/data/top/list1=a,,foo
* @see xmldb_put
*/
static int
xmldb_put_restconf_api_path(clicon_handle h,
char *db,
enum operation_type op,
char *api_path,
cxobj *xt)
{
int retval = -1;
yang_stmt *y = NULL;
yang_stmt *ykey;
char **vec = NULL;
int nvec;
int i;
char *name;
cg_var *cvi;
cvec *cvk = NULL; /* vector of index keys */
char *val2;
cbuf *ckey=NULL; /* partial keys */
cbuf *csubkey=NULL; /* partial keys */
cbuf *crx=NULL; /* partial keys */
char *keyname;
int exists;
int npairs;
struct db_pair *pairs;
yang_spec *yspec;
yang_stmt *ys;
char *filename = NULL;
char *key;
char *keys;
yspec = clicon_dbspec_yang(h);
if (db2file(h, db, &filename) < 0)
goto done;
if (api_path == NULL || *api_path!='/'){
clicon_err(OE_DB, 0, "Invalid api path: %s", api_path);
goto done;
}
if ((ckey = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if ((csubkey = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if ((vec = clicon_strsep(api_path, "/", &nvec)) == NULL)
goto done;
if (nvec < 2){
clicon_err(OE_XML, 0, "Malformed key: %s", api_path);
goto done;
}
i = 1;
while (i<nvec){
name = vec[i];
if ((keys = index(name, '=')) != NULL){
*keys = '\0';
keys++;
}
if (i==1){
if (!strlen(name) && (op==OP_DELETE || op == OP_REMOVE)){
/* Special handling of "/" */
cprintf(ckey, "/%s", name);
break;
}
else
if ((y = yang_find_topnode(yspec, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
goto done;
}
}
else
if ((y = yang_find_syntax((yang_node*)y, name)) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", name);
goto done;
}
if ((op==OP_DELETE || op == OP_REMOVE) &&
y->ys_keyword == Y_LEAF &&
y->ys_parent->yn_keyword == Y_LIST &&
yang_key_match(y->ys_parent, y->ys_argument))
/* Special rule if key, dont write last key-name, rm whole*/;
else
cprintf(ckey, "/%s", name);
i++;
switch (y->ys_keyword){
case Y_LEAF_LIST:
/* For leaf-list 'keys' is value, see 3.5.1 in restconf draft */
val2 = keys;
cprintf(ckey, "/%s", keys);
break;
case Y_LIST:
if ((ykey = yang_find((yang_node*)y, Y_KEY, NULL)) == NULL){
clicon_err(OE_XML, errno, "%s: List statement \"%s\" has no key",
__FUNCTION__, y->ys_argument);
goto done;
}
/* The value is a list of keys: <key>[ <key>]* */
if ((cvk = yang_arg2cvec(ykey, " ")) == NULL)
goto done;
cvi = NULL;
/* Iterate over individual yang keys */
while ((cvi = cvec_each(cvk, cvi)) != NULL) {
keyname = cv_string_get(cvi);
// val2 = vec[i++]; /* No */
val2 = keys;
if (i>nvec){ /* XXX >= ? */
clicon_err(OE_XML, errno, "List %s without argument", name);
goto done;
}
cprintf(ckey, "=%s", val2);
cbuf_reset(csubkey);
cprintf(csubkey, "%s/%s", cbuf_get(ckey), keyname);
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
if (db_set(filename, cbuf_get(csubkey), val2, strlen(val2)+1) < 0)
goto done;
}
if (cvk){
cvec_free(cvk);
cvk = NULL;
}
break;
default:
if (op == OP_MERGE || op == OP_REPLACE || op == OP_CREATE)
if (db_set(filename, cbuf_get(ckey), NULL, 0) < 0)
goto done;
break;
}
}
key = cbuf_get(ckey);
/* final key */
switch (op){
case OP_CREATE:
if ((exists = db_exists(filename, key)) < 0)
goto done;
if (exists == 1){
clicon_err(OE_DB, 0, "OP_CREATE: %s already exists in database", key);
goto done;
}
case OP_MERGE:
case OP_REPLACE:
if (xt==NULL){
clicon_err(OE_DB, 0, "%s: no xml when yang node %s required",
__FUNCTION__, y->ys_argument);
goto done;
}
if ((ys = yang_find_syntax((yang_node*)y, xml_name(xt))) == NULL){
clicon_err(OE_DB, 0, "%s: child %s not found under node %s",
__FUNCTION__, xml_name(xt), y->ys_argument);
goto done;
}
y = ys;
if (put(filename, xt, y, op, key) < 0)
goto done;
break;
case OP_DELETE:
if ((exists = db_exists(filename, key)) < 0)
goto done;
if (exists == 0){
clicon_err(OE_DB, 0, "OP_DELETE: %s does not exists in database", key);
goto done;
}
case OP_REMOVE:
/* Read in complete database (this can be optimized) */
if ((crx = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
cprintf(crx, "^%s.*$", key);
if ((npairs = db_regexp(filename, cbuf_get(crx), __FUNCTION__, &pairs, 0)) < 0)
goto done;
for (i = 0; i < npairs; i++) {
if (db_del(filename, pairs[i].dp_key) < 0)
goto done;
}
break;
default:
break;
}
retval = 0;
done:
// clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
if (filename)
free(filename);
if (ckey)
cbuf_free(ckey);
if (csubkey)
cbuf_free(csubkey);
if (crx)
cbuf_free(crx);
if (cvk)
cvec_free(cvk);
if (vec)
free(vec);
unchunk_group(__FUNCTION__);
return retval;
}
/*! Modify database provided an xml tree and an operation
*
* @param[in] h CLICON handle
* @param[in] db running or candidate
* @param[in] xt xml-tree. Top-level symbol is dummy
* @param[in] op OP_MERGE: just add it.
* OP_REPLACE: first delete whole database
* OP_NONE: operation attribute in xml determines operation
* @param[in] api_path According to restconf (Sec 3.5.1.1 in [restconf-draft 13])
* @retval 0 OK
* @retval -1 Error
* The xml may contain the "operation" attribute which defines the operation.
* @code
* cxobj *xt;
* if (clicon_xml_parse_str("<a>17</a>", &xt) < 0)
* err;
* if (xmldb_put(h, "running", OP_MERGE, NULL, xt) < 0)
* err;
* @endcode
* @see xmldb_put_xkey for single key
*/
int
kv_put(clicon_handle h,
char *db,
enum operation_type op,
char *api_path,
cxobj *xt)
{
int retval = -1;
cxobj *x = NULL;
yang_stmt *ys;
yang_spec *yspec;
char *dbfilename = NULL;
if (xml_child_nr(xt)==0 || xml_body(xt)!= NULL)
return xmldb_put_xkey(h, db, op, api_path, xml_body(xt));
yspec = clicon_dbspec_yang(h);
if (db2file(h, db, &dbfilename) < 0)
goto done;
if (op == OP_REPLACE){
if (db_delete(dbfilename) < 0)
goto done;
if (db_init(dbfilename) < 0)
goto done;
}
while ((x = xml_child_each(xt, x, CX_ELMNT)) != NULL){
if (api_path && strlen(api_path)){
if (xmldb_put_restconf_api_path(h, db, op, api_path, x) < 0)
goto done;
continue;
}
if ((ys = yang_find_topnode(yspec, xml_name(x))) == NULL){
clicon_err(OE_UNIX, errno, "No yang node found: %s", xml_name(x));
goto done;
}
if (put(dbfilename, /* database name */
x, /* xml root node */
ys, /* yang statement of xml node */
op, /* operation, eg merge/delete */
"" /* aggregate xml key */
) < 0)
goto done;
}
retval = 0;
done:
if (dbfilename)
free(dbfilename);
return retval;
}
/*! Raw dump of database, just keys and values, no xml interpretation
* @param[in] f File
* @param[in] dbfile File-name of database. This is a local file
* @param[in] rxkey Key regexp, eg "^.*$"
* @note This function can only be called locally.
*/
int
kv_dump(FILE *f,
char *dbfilename,
char *rxkey)
{
int retval = -1;
int npairs;
struct db_pair *pairs;
/* Default is match all */
if (rxkey == NULL)
rxkey = "^.*$";
/* Get all keys/values for vector */
if ((npairs = db_regexp(dbfilename, rxkey, __FUNCTION__, &pairs, 0)) < 0)
goto done;
for (npairs--; npairs >= 0; npairs--)
fprintf(f, "%s %s\n", pairs[npairs].dp_key,
pairs[npairs].dp_val?pairs[npairs].dp_val:"");
retval = 0;
done:
unchunk_group(__FUNCTION__);
return retval;
}
/*! Copy database from db1 to db2
* @param[in] h Clicon handle
* @param[in] from Source database copy
* @param[in] to Destination database
* @retval -1 Error
* @retval 0 OK
*/
int
kv_copy(clicon_handle h,
char *from,
char *to)
{
int retval = -1;
char *fromfile = NULL;
char *tofile = NULL;
/* XXX lock */
if (db2file(h, from, &fromfile) < 0)
goto done;
if (db2file(h, to, &tofile) < 0)
goto done;
if (clicon_file_copy(fromfile, tofile) < 0)
goto done;
retval = 0;
done:
if (fromfile)
free(fromfile);
if (tofile)
free(tofile);
return retval;
}
/*! Lock database
* @param[in] h Clicon handle
* @param[in] db Database
* @param[in] pid Process id
* @retval -1 Error
* @retval 0 OK
*/
int
kv_lock(clicon_handle h,
char *db,
int pid)
{
int retval = -1;
if (db_islocked(db)){
if (pid != db_islocked(db)){
clicon_err(OE_DB, 0, "lock failed: locked by %d", db_islocked(db));
goto done;
}
}
else
db_lock(db, pid);
retval = 0;
done:
return retval;
}
/*! Unlock database
* @param[in] h Clicon handle
* @param[in] db Database
* @param[in] pid Process id
* @retval -1 Error
* @retval 0 OK
*/
int
kv_unlock(clicon_handle h,
char *db,
int pid)
{
int retval = -1;
int pid1;
pid1 = db_islocked(db);
if (pid1){
if (pid == pid1)
db_unlock(db);
else{
clicon_err(OE_DB, 0, "unlock failed: locked by %d", pid1);
goto done;
}
}
retval = 0;
done:
return retval;
}
/*! Unlock all databases locked by pid (eg process dies)
*/
int
kv_unlock_all(clicon_handle h,
int pid)
{
return db_unlock_all(pid);
}
/*! Check if database is locked
* @param[in] h Clicon handle
* @param[in] db Database
* @retval -1 Error
* @retval 0 Not locked
* @retval >0 Id of locker
*/
int
kv_islocked(clicon_handle h,
char *db)
{
return db_islocked(db);
}
/*! Check if db exists
* @param[in] h Clicon handle
* @param[in] db Database
* @retval -1 Error
* @retval 0 No it does not exist
* @retval 1 Yes it exists
*/
int
kv_exists(clicon_handle h,
char *db)
{
int retval = -1;
char *filename = NULL;
struct stat sb;
if (db2file(h, db, &filename) < 0)
goto done;
if (lstat(filename, &sb) < 0)
retval = 0;
else
retval = 1;
done:
if (filename)
free(filename);
return retval;
}
/*! Delete database. Remove file
* @param[in] h Clicon handle
* @param[in] db Database
* @retval -1 Error
* @retval 0 OK
*/
int
kv_delete(clicon_handle h,
char *db)
{
int retval = -1;
char *filename = NULL;
if (db2file(h, db, &filename) < 0)
goto done;
if (db_delete(filename) < 0)
goto done;
retval = 0;
done:
if (filename)
free(filename);
return retval;
}
/*! Initialize database
* @param[in] h Clicon handle
* @param[in] db Database
* @retval 0 OK
* @retval -1 Error
*/
int
kv_init(clicon_handle h,
char *db)
{
int retval = -1;
char *filename = NULL;
if (db2file(h, db, &filename) < 0)
goto done;
if (db_init(filename) < 0)
goto done;
retval = 0;
done:
if (filename)
free(filename);
return retval;
}
/*! plugin init function */
int
keyvalue_plugin_exit(void)
{
return 0;
}
static const struct xmldb_api api;
/*! plugin init function */
void *
clixon_xmldb_plugin_init(int version)
{
if (version != XMLDB_API_VERSION){
clicon_err(OE_DB, 0, "Invalid version %d expected %d",
version, XMLDB_API_VERSION);
goto done;
}
return (void*)&api;
done:
return NULL;
}
static const struct xmldb_api api = {
1,
XMLDB_API_MAGIC,
clixon_xmldb_plugin_init,
keyvalue_plugin_exit,
kv_get,
kv_put,
kv_dump,
kv_copy,
kv_lock,
kv_unlock,
kv_unlock_all,
kv_islocked,
kv_exists,
kv_delete,
kv_init,
};
#if 0 /* Test program */
/*
* Turn this on to get an xpath test program
* Usage: clicon_xpath [<xpath>]
* read xml from input
* Example compile:
gcc -g -o xmldb -I. -I../clixon ./clixon_xmldb.c -lclixon -lcligen
*/
static int
usage(char *argv0)
{
fprintf(stderr, "usage:\n%s\tget <db> <yangdir> <yangmod> [<xpath>]\t\txml on stdin\n", argv0);
fprintf(stderr, "\tput <db> <yangdir> <yangmod> set|merge|delete\txml to stdout\n");
exit(0);
}
int
main(int argc, char **argv)
{
cxobj *xt;
cxobj *xn;
char *xpath;
enum operation_type op;
char *cmd;
char *db;
char *yangdir;
char *yangmod;
yang_spec *yspec = NULL;
clicon_handle h;
if ((h = clicon_handle_init()) == NULL)
goto done;
clicon_log_init("xmldb", LOG_DEBUG, CLICON_LOG_STDERR);
if (argc < 4){
usage(argv[0]);
goto done;
}
cmd = argv[1];
db = argv[2];
yangdir = argv[3];
yangmod = argv[4];
db_init(db);
if ((yspec = yspec_new()) == NULL)
goto done
if (yang_parse(h, yangdir, yangmod, NULL, yspec) < 0)
goto done;
if (strcmp(cmd, "get")==0){
if (argc < 5)
usage(argv[0]);
xpath = argc>5?argv[5]:NULL;
if (xmldb_get(h, db, xpath, &xt, NULL, NULL) < 0)
goto done;
clicon_xml2file(stdout, xt, 0, 1);
}
else
if (strcmp(cmd, "put")==0){
if (argc != 6)
usage(argv[0]);
if (clicon_xml_parse_file(0, &xt, "</clicon>") < 0)
goto done;
if (xml_rootchild(xt, 0, &xn) < 0)
goto done;
if (strcmp(argv[5], "set") == 0)
op = OP_REPLACE;
else
if (strcmp(argv[4], "merge") == 0)
op = OP_MERGE;
else if (strcmp(argv[5], "delete") == 0)
op = OP_REMOVE;
else
usage(argv[0]);
if (xmldb_put(h, db, op, NULL, xn) < 0)
goto done;
}
else
usage(argv[0]);
printf("\n");
done:
return 0;
}
#endif /* Test program */