From b1c74b5f1f55693810d83cdcae03a370db53cab5 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Sun, 10 Mar 2019 17:27:52 +0100 Subject: [PATCH] * Ensured you can add multiple callbacks for any RPC, including basic ones. * Extra RPC:s will be called _after_ the basic ones. * One specific usecase is hook for `copy-config` (see [doc/ROADMAP.md] that can be implemented thus way. * `rpc_callback_register` added a namespace parameter. Example: ``` rpc_callback_register(h, empty_rpc, NULL, "urn:example:clixon", "empty"); ``` --- CHANGELOG.md | 9 +- apps/backend/backend_client.c | 934 ++++++++++++++++--------------- apps/backend/backend_client.h | 1 + apps/backend/backend_commit.c | 117 ++-- apps/backend/backend_commit.h | 8 +- apps/backend/backend_main.c | 4 + apps/netconf/netconf_rpc.c | 3 +- apps/restconf/restconf_methods.c | 2 +- doc/FAQ.md | 30 +- doc/ROADMAP.md | 2 +- docker/base/Makefile.in | 4 +- example/example_backend.c | 25 +- example/example_netconf.c | 3 +- example/example_restconf.c | 2 +- lib/clixon/clixon_netconf_lib.h | 1 + lib/clixon/clixon_plugin.h | 12 +- lib/src/clixon_netconf_lib.c | 37 +- lib/src/clixon_plugin.c | 75 ++- 18 files changed, 755 insertions(+), 514 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0de109a..cce6e450 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 3.10.0 (Upcoming) ### Major New features -* CLI history: [Preserve CLI command history across sessions. The up/down arrows](https://github.com/clicon/clixon/issues/79) +* Persistent CLI history: [Preserve CLI command history across sessions. The up/down arrows](https://github.com/clicon/clixon/issues/79) * The design is similar to bash history: * The CLI loads/saves its complete history to a file on entry and exit, respectively * The size (number of lines) of the file is the same as the history in memory @@ -28,6 +28,10 @@ * Note that this adds bytes to your configs ### API changes on existing features (you may need to change your code) +* `rpc_callback_register` added a namespace parameter. Example: + ``` + rpc_callback_register(h, empty_rpc, NULL, "urn:example:clixon", "empty"); + ``` * Clixon configuration file top-level symbols has changed to `clixon-config`and namespace check is enforced. This means all Clixon configuration files must change from: ``` @@ -46,6 +50,9 @@ to: ``` ### Minor changes +* Ensured you can add multiple callbacks for any RPC, including basic ones. + * Extra RPC:s will be called _after_ the basic ones. + * One specific usecase is hook for `copy-config` (see [doc/ROADMAP.md] that can be implemented thus way. * Added "base" as CLI default mode and "cli> " as default prompt. * clixon-config YAML file has new revision: 2019-03-05. * New URN and changed top-level symbol to `clixon-config` diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c index c6d407ac..49420687 100644 --- a/apps/backend/backend_client.c +++ b/apps/backend/backend_client.c @@ -150,102 +150,6 @@ backend_client_rm(clicon_handle h, return backend_client_delete(h, ce); /* actually purge it */ } -/*! Find target/source in netconf request. Assume sanity- not finding is error */ -static char* -netconf_db_find(cxobj *xn, - char *name) -{ - cxobj *xs; /* source */ - cxobj *xi; - char *db = NULL; - - if ((xs = xml_find(xn, name)) == NULL) - goto done; - if ((xi = xml_child_i(xs, 0)) == NULL) - goto done; - db = xml_name(xi); - done: - return db; -} - -/*! Internal message: get-config - * - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[out] cbret Return xml value cligen buffer - */ -static int -from_client_get_config(clicon_handle h, - cxobj *xe, - char *username, - cbuf *cbret) -{ - int retval = -1; - char *db; - cxobj *xfilter; - char *xpath = "/"; - cxobj *xret = NULL; - cbuf *cbx = NULL; /* Assist cbuf */ - cxobj *xnacm = NULL; - cxobj **xvec = NULL; - size_t xlen; - int ret; - - if ((db = netconf_db_find(xe, "source")) == NULL){ - clicon_err(OE_XML, 0, "db not found"); - goto done; - } - if (xmldb_validate_db(db) < 0){ - if ((cbx = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - goto done; - } - cprintf(cbx, "No such database: %s", db); - if (netconf_invalid_value(cbret, "protocol", cbuf_get(cbx))< 0) - goto done; - goto ok; - } - if ((xfilter = xml_find(xe, "filter")) != NULL) - if ((xpath = xml_find_value(xfilter, "select"))==NULL) - xpath="/"; - if (xmldb_get(h, db, xpath, 1, &xret, NULL) < 0){ - if (netconf_operation_failed(cbret, "application", "read registry")< 0) - goto done; - goto ok; - } - /* Pre-NACM access step */ - if ((ret = nacm_access_pre(h, username, &xnacm)) < 0) - goto done; - if (ret == 0){ /* Do NACM validation */ - if (xpath_vec(xret, "%s", &xvec, &xlen, xpath?xpath:"/") < 0) - goto done; - /* NACM datanode/module read validation */ - if (nacm_datanode_read(xret, xvec, xlen, username, xnacm) < 0) - goto done; - } - cprintf(cbret, ""); - if (xret==NULL) - cprintf(cbret, ""); - else{ - if (xml_name_set(xret, "data") < 0) - goto done; - if (clicon_xml2cbuf(cbret, xret, 0, 0) < 0) - goto done; - } - cprintf(cbret, ""); - ok: - retval = 0; - done: - if (xnacm) - xml_free(xnacm); - if (xvec) - free(xvec); - if (cbx) - cbuf_free(cbx); - if (xret) - xml_free(xret); - return retval; -} /*! Get streams state according to RFC 8040 or RFC5277 common function * @param[in] h Clicon handle @@ -304,7 +208,6 @@ client_get_streams(clicon_handle h, return retval; } - /*! Get system state-data, including streams and plugins * @param[in] h Clicon handle * @param[in] xpath Xpath selection, not used but may be to filter early @@ -365,44 +268,55 @@ client_statedata(clicon_handle h, return retval; } -/*! Internal message: get +/*! Retrieve all or part of a specified configuration. * - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[out] cbret Return xml value cligen buffer - * @see from_client_get_config + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., "); /* OK */ + cprintf(cbret, ""); if (xret==NULL) cprintf(cbret, ""); else{ @@ -433,26 +347,33 @@ from_client_get(clicon_handle h, xml_free(xnacm); if (xvec) free(xvec); + if (cbx) + cbuf_free(cbx); if (xret) xml_free(xret); return retval; } -/*! Internal message: edit-config +/*! Loads all or part of a specified configuration to target configuration * - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] mypid Process/session id of calling client - * @param[out] cbret Return xml value cligen buffer + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; char *target; cxobj *xc; cxobj *x; @@ -462,7 +383,9 @@ from_client_edit_config(clicon_handle h, yang_spec *yspec; cbuf *cbx = NULL; /* Assist cbuf */ int ret; + char *username; + username = clicon_username_get(h); if ((yspec = clicon_dbspec_yang(h)) == NULL){ clicon_err(OE_YANG, ENOENT, "No yang spec9"); goto done; @@ -544,190 +467,14 @@ from_client_edit_config(clicon_handle h, } /* from_client_edit_config */ -/*! Internal message: Lock database - * - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] pid Unix process id - * @param[out] cbret Return xml value cligen buffer - */ -static int -from_client_lock(clicon_handle h, - cxobj *xe, - int pid, - cbuf *cbret) -{ - int retval = -1; - char *db; - int piddb; - cbuf *cbx = NULL; /* Assist cbuf */ - - if ((db = netconf_db_find(xe, "target")) == NULL){ - if (netconf_missing_element(cbret, "protocol", "target", NULL) < 0) - goto done; - goto ok; - } - if ((cbx = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - goto done; - } - if (xmldb_validate_db(db) < 0){ - cprintf(cbx, "No such database: %s", db); - if (netconf_invalid_value(cbret, "protocol", cbuf_get(cbx))< 0) - goto done; - goto ok; - } - /* - * A lock MUST not be granted if either of the following conditions is true: - * 1) A lock is already held by any NETCONF session or another entity. - * 2) The target configuration is , it has already been modified, and - * these changes have not been committed or rolled back. - */ - piddb = xmldb_islocked(h, db); - if (piddb){ - cprintf(cbx, "%d", piddb); - if (netconf_lock_denied(cbret, cbuf_get(cbx), "Operation failed, lock is already held") < 0) - goto done; - goto ok; - } - else{ - if (xmldb_lock(h, db, pid) < 0) - goto done; - cprintf(cbret, ""); - } - ok: - retval = 0; - done: - if (cbx) - cbuf_free(cbx); - return retval; -} - -/*! Internal message: Unlock database - * - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] pid Unix process id - * @param[out] cbret Return xml value cligen buffer - */ -static int -from_client_unlock(clicon_handle h, - cxobj *xe, - int pid, - cbuf *cbret) -{ - int retval = -1; - char *db; - int piddb; - cbuf *cbx = NULL; /* Assist cbuf */ - - if ((db = netconf_db_find(xe, "target")) == NULL){ - if (netconf_missing_element(cbret, "protocol", "target", NULL) < 0) - goto done; - goto ok; - } - if ((cbx = cbuf_new()) == NULL){ - clicon_err(OE_XML, errno, "cbuf_new"); - goto done; - } - if (xmldb_validate_db(db) < 0){ - cprintf(cbx, "No such database: %s", db); - if (netconf_invalid_value(cbret, "protocol", cbuf_get(cbx))< 0) - goto done; - goto ok; - } - piddb = xmldb_islocked(h, db); - /* - * An unlock operation will not succeed if any of the following - * conditions are true: - * 1) the specified lock is not currently active - * 2) the session issuing the operation is not the same - * session that obtained the lock - */ - if (piddb==0 || piddb != pid){ - cprintf(cbx, "pid=%d piddb=%d", pid, piddb); - if (netconf_lock_denied(cbret, cbuf_get(cbx), "Unlock failed, lock is already held") < 0) - goto done; - goto ok; - } - else{ - xmldb_unlock(h, db); - if (cprintf(cbret, "") < 0) - goto done; - } - ok: - retval = 0; - done: - if (cbx) - cbuf_free(cbx); - return retval; -} - -/*! Internal message: Kill session (Kill the process) - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK - * @retval -1 Error. Send error message back to client. - */ -static int -from_client_kill_session(clicon_handle h, - cxobj *xe, - cbuf *cbret) -{ - int retval = -1; - uint32_t pid; /* other pid */ - char *str; - struct client_entry *ce; - char *db = "running"; /* XXX */ - cxobj *x; - - if ((x = xml_find(xe, "session-id")) == NULL || - (str = xml_find_value(x, "body")) == NULL){ - if (netconf_missing_element(cbret, "protocol", "session-id", NULL) < 0) - goto done; - goto ok; - } - pid = atoi(str); - /* may or may not be in active client list, probably not */ - if ((ce = ce_find_bypid(backend_client_list(h), pid)) != NULL){ - xmldb_unlock_all(h, pid); - backend_client_rm(h, ce); - } - - if (kill (pid, 0) != 0 && errno == ESRCH) /* Nothing there */ - ; - else{ - killpg(pid, SIGTERM); - kill(pid, SIGTERM); -#if 0 /* Hate sleeps we assume it died, see also 0 in next if.. */ - sleep(1); -#endif - } - if (1 || (kill (pid, 0) != 0 && errno == ESRCH)){ /* Nothing there */ - /* clear from locks */ - if (xmldb_islocked(h, db) == pid) - xmldb_unlock(h, db); - } - else{ /* failed to kill client */ - if (netconf_operation_failed(cbret, "application", "Failed to kill session")< 0) - goto done; - goto ok; - } - cprintf(cbret, ""); - ok: - retval = 0; - done: - return retval; -} - -/*! Internal message: Copy database from db1 to db2 - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] mypid Process/session id of calling client - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK - * @retval -1 Error. Send error message back to client. +/*! Create or replace an entire config with another complete config db + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., only exec permission * else: * - omit data nodes to which the client does not have read access @@ -736,13 +483,16 @@ from_client_kill_session(clicon_handle h, static int from_client_copy_config(clicon_handle h, cxobj *xe, - int mypid, - cbuf *cbret) + cbuf *cbret, + void *arg, + void *regarg) { + int retval = -1; + struct client_entry *ce = (struct client_entry *)arg; char *source; char *target; - int retval = -1; int piddb; + int mypid = ce->ce_pid; cbuf *cbx = NULL; /* Assist cbuf */ if ((source = netconf_db_find(xe, "source")) == NULL){ @@ -793,26 +543,30 @@ from_client_copy_config(clicon_handle h, return retval; } -/*! Internal message: Delete database - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] mypid Process/session id of calling client - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK - * @retval -1 Error. Send error message back to client. +/*! Delete a configuration datastore. + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; - if ((target = netconf_db_find(xe, "target")) == NULL|| + if ((target = netconf_db_find(xe, "target")) == NULL || strcmp(target, "running")==0){ if (netconf_missing_element(cbret, "protocol", "target", NULL) < 0) goto done; @@ -855,13 +609,314 @@ from_client_delete_config(clicon_handle h, return retval; } -/*! Internal message: Create subscription for notifications see RFC 5277 - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[in] ce Client entry - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK - * @retval -1 Error. Send error message back to client. +/*! Lock the configuration system of a device + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; + char *db; + int piddb; + cbuf *cbx = NULL; /* Assist cbuf */ + + if ((db = netconf_db_find(xe, "target")) == NULL){ + if (netconf_missing_element(cbret, "protocol", "target", NULL) < 0) + goto done; + goto ok; + } + if ((cbx = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (xmldb_validate_db(db) < 0){ + cprintf(cbx, "No such database: %s", db); + if (netconf_invalid_value(cbret, "protocol", cbuf_get(cbx))< 0) + goto done; + goto ok; + } + /* + * A lock MUST not be granted if either of the following conditions is true: + * 1) A lock is already held by any NETCONF session or another entity. + * 2) The target configuration is , it has already been modified, and + * these changes have not been committed or rolled back. + */ + piddb = xmldb_islocked(h, db); + if (piddb){ + cprintf(cbx, "%d", piddb); + if (netconf_lock_denied(cbret, cbuf_get(cbx), "Operation failed, lock is already held") < 0) + goto done; + goto ok; + } + else{ + if (xmldb_lock(h, db, pid) < 0) + goto done; + cprintf(cbret, ""); + } + ok: + retval = 0; + done: + if (cbx) + cbuf_free(cbx); + return retval; +} + +/*! Release a configuration lock previously obtained with the 'lock' operation + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; + char *db; + int piddb; + cbuf *cbx = NULL; /* Assist cbuf */ + + if ((db = netconf_db_find(xe, "target")) == NULL){ + if (netconf_missing_element(cbret, "protocol", "target", NULL) < 0) + goto done; + goto ok; + } + if ((cbx = cbuf_new()) == NULL){ + clicon_err(OE_XML, errno, "cbuf_new"); + goto done; + } + if (xmldb_validate_db(db) < 0){ + cprintf(cbx, "No such database: %s", db); + if (netconf_invalid_value(cbret, "protocol", cbuf_get(cbx))< 0) + goto done; + goto ok; + } + piddb = xmldb_islocked(h, db); + /* + * An unlock operation will not succeed if any of the following + * conditions are true: + * 1) the specified lock is not currently active + * 2) the session issuing the operation is not the same + * session that obtained the lock + */ + if (piddb==0 || piddb != pid){ + cprintf(cbx, "pid=%d piddb=%d", pid, piddb); + if (netconf_lock_denied(cbret, cbuf_get(cbx), "Unlock failed, lock is already held") < 0) + goto done; + goto ok; + } + else{ + xmldb_unlock(h, db); + if (cprintf(cbret, "") < 0) + goto done; + } + ok: + retval = 0; + done: + if (cbx) + cbuf_free(cbx); + return retval; +} + +/*! Retrieve running configuration and device state information. + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., "); /* OK */ + if (xret==NULL) + cprintf(cbret, ""); + else{ + if (xml_name_set(xret, "data") < 0) + goto done; + if (clicon_xml2cbuf(cbret, xret, 0, 0) < 0) + goto done; + } + cprintf(cbret, ""); + ok: + retval = 0; + done: + if (xnacm) + xml_free(xnacm); + if (xvec) + free(xvec); + if (xret) + xml_free(xret); + return retval; +} + +/*! Request graceful termination of a NETCONF session. + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; + + xmldb_unlock_all(h, pid); + stream_ss_delete_all(h, ce_event_cb, (void*)ce); + cprintf(cbret, ""); + return 0; +} + +/*! Internal message: Force the termination of a NETCONF session. + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., "); + ok: + retval = 0; + done: + return retval; +} + +/*! Create a notification subscription + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., @@ -872,21 +927,23 @@ from_client_delete_config(clicon_handle h, * */ static int -from_client_create_subscription(clicon_handle h, - cxobj *xe, - struct client_entry *ce, - cbuf *cbret) +from_client_create_subscription(clicon_handle h, + cxobj *xe, + cbuf *cbret, + void *arg, + void *regarg) { - char *stream = "NETCONF"; - int retval = -1; - cxobj *x; /* Generic xml tree */ - cxobj *xfilter; /* Filter xml tree */ - char *ftype; - char *starttime = NULL; - char *stoptime = NULL; - char *selector = NULL; - struct timeval start; - struct timeval stop; + int retval = -1; + struct client_entry *ce = (struct client_entry *)arg; + char *stream = "NETCONF"; + cxobj *x; /* Generic xml tree */ + cxobj *xfilter; /* Filter xml tree */ + char *ftype; + char *starttime = NULL; + char *stoptime = NULL; + char *selector = NULL; + struct timeval start; + struct timeval stop; if ((x = xpath_first(xe, "//stream")) != NULL) stream = xml_find_value(x, "body"); @@ -945,17 +1002,21 @@ from_client_create_subscription(clicon_handle h, return retval; } -/*! Internal message: Set debug level. This is global, not just for the session. - * @param[in] h Clicon handle - * @param[in] xe Netconf request xml tree - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK - * @retval -1 Error. Send error message back to client. +/*! Set debug level. + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; yspec = clicon_dbspec_yang(h); /* Return netconf message. Should be filled in by the dispatch(sub) functions * as wither rpc-error or by positive response. @@ -1067,81 +1125,17 @@ from_client_msg(clicon_handle h, if (ret == 0) /* Not permitted and cbret set */ goto reply; } - if (strcmp(rpc, "get-config") == 0){ - if (from_client_get_config(h, xe, username, cbret) <0) + clicon_err_reset(); + if ((ret = rpc_callback_call(h, xe, cbret, ce)) < 0){ + if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0) goto done; + clicon_log(LOG_NOTICE, "%s Error in rpc_callback_call:%s", __FUNCTION__, xml_name(xe)); + goto reply; /* Dont quit here on user callbacks */ } - else if (strcmp(rpc, "edit-config") == 0){ - if (from_client_edit_config(h, xe, pid, username, cbret) <0) + if (ret == 0){ /* not handled by callback */ + if (netconf_operation_failed(cbret, "application", "Callback not recognized")< 0) goto done; - } - else if (strcmp(rpc, "copy-config") == 0){ - if (from_client_copy_config(h, xe, pid, cbret) <0) - goto done; - } - else if (strcmp(rpc, "delete-config") == 0){ - if (from_client_delete_config(h, xe, pid, cbret) <0) - goto done; - } - else if (strcmp(rpc, "lock") == 0){ - if (from_client_lock(h, xe, pid, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "unlock") == 0){ - if (from_client_unlock(h, xe, pid, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "get") == 0){ - if (from_client_get(h, xe, username, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "close-session") == 0){ - xmldb_unlock_all(h, pid); - stream_ss_delete_all(h, ce_event_cb, (void*)ce); - cprintf(cbret, ""); - } - else if (strcmp(rpc, "kill-session") == 0){ - if (from_client_kill_session(h, xe, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "validate") == 0){ - if ((db = netconf_db_find(xe, "source")) == NULL){ - if (netconf_missing_element(cbret, "protocol", "source", NULL) < 0) - goto done; - goto reply; - } - if (from_client_validate(h, db, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "commit") == 0){ - if (from_client_commit(h, pid, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "discard-changes") == 0){ - if (from_client_discard_changes(h, pid, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "create-subscription") == 0){ - if (from_client_create_subscription(h, xe, ce, cbret) < 0) - goto done; - } - else if (strcmp(rpc, "debug") == 0){ - if (from_client_debug(h, xe, cbret) < 0) - goto done; - } - else{ - clicon_err_reset(); - if ((ret = rpc_callback_call(h, xe, cbret, ce)) < 0){ - if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0) - goto done; - clicon_log(LOG_NOTICE, "%s Error in rpc_callback_call:%s", __FUNCTION__, xml_name(xe)); - goto reply; /* Dont quit here on user callbacks */ - } - if (ret == 0){ /* not handled by callback */ - if (netconf_operation_failed(cbret, "application", "Callback not recognized")< 0) - goto done; - goto reply; - } + goto reply; } } reply: @@ -1217,3 +1211,71 @@ from_client(int s, free(msg); return retval; /* -1 here terminates backend */ } + +/*! Init backend rpc: Set up standard netconf rpc callbacks + * @param[in] h Clicon handle + * @retval -1 Error (fatal) + * @retval 0 OK + * @see ietf-netconf@2011-06-01.yang + */ +int +backend_rpc_init(clicon_handle h) +{ + int retval = -1; + + /* In backend_client.? RFC 6241 */ + if (rpc_callback_register(h, from_client_get_config, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "get-config") < 0) + goto done; + if (rpc_callback_register(h, from_client_edit_config, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "edit-config") < 0) + goto done; + if (rpc_callback_register(h, from_client_copy_config, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "copy-config") < 0) + goto done; + if (rpc_callback_register(h, from_client_delete_config, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "delete-config") < 0) + goto done; + if (rpc_callback_register(h, from_client_lock, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "lock") < 0) + goto done; + if (rpc_callback_register(h, from_client_unlock, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "unlock") < 0) + goto done; + if (rpc_callback_register(h, from_client_get, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "get") < 0) + goto done; + if (rpc_callback_register(h, from_client_close_session, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "close-session") < 0) + goto done; + if (rpc_callback_register(h, from_client_kill_session, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "kill-session") < 0) + goto done; + /* In backend_commit.? */ + if (rpc_callback_register(h, from_client_commit, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "commit") < 0) + goto done; + if (rpc_callback_register(h, from_client_discard_changes, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "discard-changes") < 0) + goto done; + /* if-feature confirmed-commit */ + if (rpc_callback_register(h, from_client_cancel_commit, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "cancel-commit") < 0) + goto done; + /* if-feature validate */ + if (rpc_callback_register(h, from_client_validate, NULL, + "urn:ietf:params:xml:ns:netconf:base:1.0", "validate") < 0) + goto done; + + /* In backend_client.? RPC from RFC 5277 */ + if (rpc_callback_register(h, from_client_create_subscription, NULL, + "urn:ietf:params:xml:ns:netmod:notification", "create-subscription") < 0) + goto done; + /* In backend_client.? Clixon RPC */ + if (rpc_callback_register(h, from_client_debug, NULL, + "http://clicon.org/lib", "debug") < 0) + goto done; + retval =0; + done: + return retval; +} diff --git a/apps/backend/backend_client.h b/apps/backend/backend_client.h index c9a1da11..5aad6843 100644 --- a/apps/backend/backend_client.h +++ b/apps/backend/backend_client.h @@ -60,5 +60,6 @@ struct client_entry{ */ int backend_client_rm(clicon_handle h, struct client_entry *ce); int from_client(int fd, void *arg); +int backend_rpc_init(clicon_handle h); #endif /* _BACKEND_CLIENT_H_ */ diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c index d53ce431..accc2641 100644 --- a/apps/backend/backend_commit.c +++ b/apps/backend/backend_commit.c @@ -330,8 +330,6 @@ startup_validate(clicon_handle h, goto done; } - - /*! Do a diff between candidate and running, then start a commit transaction * * The code reverts changes if the commit fails. But if the revert @@ -408,25 +406,33 @@ candidate_commit(clicon_handle h, goto done; } -/*! Commit changes from candidate to running - * @param[in] h Clicon handle - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK. This may indicate both ok and err msg back to client - * @retval -1 (Local) Error - * NACM: The server MUST determine the exact nodes in the running +/*! Commit the candidate configuration as the device's new current configuration + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; + int piddb; + cbuf *cbx = NULL; /* Assist cbuf */ + int ret; /* Check if target locked by other client */ piddb = xmldb_islocked(h, "running"); @@ -457,22 +463,30 @@ from_client_commit(clicon_handle h, return retval; /* may be zero if we ignoring errors from commit */ } /* from_client_commit */ -/*! Discard all changes in candidate / revert to running - * @param[in] h Clicon handle - * @param[in] mypid Process/session id of calling client - * @param[out] cbret Return xml value cligen buffer +/*! Revert the candidate configuration to the current running configuration. + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., ce_pid; + int piddb; + cbuf *cbx = NULL; /* Assist cbuf */ /* Check if target locked by other client */ piddb = xmldb_islocked(h, "candidate"); @@ -500,22 +514,60 @@ from_client_discard_changes(clicon_handle h, return retval; /* may be zero if we ignoring errors from commit */ } -/*! Handle an incoming validate message from a client. - * @param[in] h Clicon handle - * @param[in] db Database name - * @param[out] cbret Return xml value cligen buffer - * @retval 0 OK. This may indicate both ok and err msg back to client (eg invalid) - * @retval -1 (Local) Error +/*! Cancel an ongoing confirmed commit. + * If the confirmed commit is persistent, the parameter 'persist-id' must be + * given, and it must match the value of the 'persist' parameter. + * + * @param[in] h Clicon handle + * @param[in] xe Request: + * @param[out] cbret Return xml tree, eg ..., + * @param[out] cbret Return xml tree, eg ..., 0){ /* Handled locally */ if (xml_parse_string(cbuf_get(cbret), NULL, xret) < 0) goto done; } @@ -692,6 +692,7 @@ netconf_rpc_dispatch(clicon_handle h, strcmp(xml_name(xe), "kill-session") == 0 || strcmp(xml_name(xe), "validate") == 0 || /* :validate */ strcmp(xml_name(xe), "commit") == 0 || /* :candidate */ + strcmp(xml_name(xe), "cancel-commit") == 0 || strcmp(xml_name(xe), "discard-changes") == 0){ if (clicon_rpc_netconf_xml(h, xml_parent(xe), xret, NULL) < 0) goto done; diff --git a/apps/restconf/restconf_methods.c b/apps/restconf/restconf_methods.c index fcc266a9..eeaca5b5 100644 --- a/apps/restconf/restconf_methods.c +++ b/apps/restconf/restconf_methods.c @@ -1674,7 +1674,7 @@ api_operations_post(clicon_handle h, */ if ((ret = rpc_callback_call(h, xbot, cbret, r)) < 0) goto done; - if (ret == 1){ /* Handled locally */ + if (ret > 0){ /* Handled locally */ if (xml_parse_string(cbuf_get(cbret), NULL, &xret) < 0) goto done; /* Local error: return it and quit */ diff --git a/doc/FAQ.md b/doc/FAQ.md index c18fb038..a42faee0 100644 --- a/doc/FAQ.md +++ b/doc/FAQ.md @@ -452,7 +452,7 @@ int clixon_plugin_init(clicon_handle h) { ... - rpc_callback_register(h, example_rpc, NULL, "example-rpc"); + rpc_callback_register(h, example_rpc, NULL, "urn:example:my", "example-rpc"); ... } ``` @@ -471,6 +471,34 @@ example_rpc(clicon_handle h, /* Clicon handle */ ``` Here, the callback is over-simplified. +## I want to add a hook to an existing operation, can I do that? + +Yes, by registering an [RPC callback](how-do-i-write-an-rpc-function) +on an existing function, your function will be called immediately +_after_ the original. + +The following example shows how `my_copy` can be called right after the system (RFC6241) `copy-config` RPC. You can perform some side-effect or even alter +the original operation: +``` +static int +my_copy(clicon_handle h, /* Clicon handle */ + cxobj *xe, /* Request: */ + cbuf *cbret, /* Reply eg ... */ + void *arg, /* Client session */ + void *regarg) /* Argument given at register */ +{ + /* Do something */ + return 0; +} +int +clixon_plugin_init(clicon_handle h) +{ +... + rpc_callback_register(h, my_copy, NULL, "urn:ietf:params:xml:ns:netconf:base:1.0", "copy-config"); +... +} +``` + ## How do I write an authentication callback? A restconf call may need to be authenticated. diff --git a/doc/ROADMAP.md b/doc/ROADMAP.md index eacc833f..13b4f458 100644 --- a/doc/ROADMAP.md +++ b/doc/ROADMAP.md @@ -13,7 +13,7 @@ - (DONE)XML [Namespace handling](https://github.com/clicon/clixon/issues/49) (DONE) ## Medium prio: -- Support a plugin callback that is invoked when copy-config is called. +- (DONE) Register extra callbacks on system Netconf messages. (Was:Support a plugin callback that is invoked when copy-config is called.) - (DONE)Preserve CLI command history across sessions. The up/down arrows - (DONE)Support for XML regex's. - Currently Posix extended regular expressions diff --git a/docker/base/Makefile.in b/docker/base/Makefile.in index 5fd5a2a7..3fa15155 100644 --- a/docker/base/Makefile.in +++ b/docker/base/Makefile.in @@ -43,7 +43,7 @@ IMG = clixon/clixon # base image SHELL = /bin/sh -.PHONY: all clean distclean docker push depend install-include install uninstall +.PHONY: all clean distclean docker push depend install-include install uninstall test all: echo "Run make docker to build docker image" @@ -58,6 +58,8 @@ clean: distclean: clean rm -f Makefile *~ .depend +test: + docker: clixon Dockerfile sudo docker build -t $(IMG) . # --no-cache diff --git a/example/example_backend.c b/example/example_backend.c index 9ee40c26..7484ab75 100644 --- a/example/example_backend.c +++ b/example/example_backend.c @@ -178,6 +178,23 @@ example_rpc(clicon_handle h, /* Clicon handle */ return retval; } +/*! This will be called as a hook right after the original system copy-config + */ +static int +example_copy_extra(clicon_handle h, /* Clicon handle */ + cxobj *xe, /* Request: */ + cbuf *cbret, /* Reply eg ... */ + void *arg, /* client_entry */ + void *regarg) /* Argument given at register */ +{ + int retval = -1; + + fprintf(stderr, "%s\n", __FUNCTION__); + retval = 0; + // done: + return retval; +} + /*! Called to get state data from plugin * @param[in] h Clicon handle * @param[in] xpath String with XPATH syntax. or NULL for all @@ -379,18 +396,22 @@ clixon_plugin_init(clicon_handle h) /* From example.yang (clicon) */ if (rpc_callback_register(h, empty_rpc, NULL, + "urn:example:clixon", "empty"/* Xml tag when callback is made */ ) < 0) goto done; /* Same as example but with optional input/output */ if (rpc_callback_register(h, example_rpc, NULL, + "urn:example:clixon", "optional"/* Xml tag when callback is made */ ) < 0) goto done; - if (rpc_callback_register(h, example_rpc, + /* Called after the regular system copy_config callback */ + if (rpc_callback_register(h, example_copy_extra, NULL, - "example"/* Xml tag when callback is made */ + "urn:ietf:params:xml:ns:netconf:base:1.0", + "copy-config" ) < 0) goto done; diff --git a/example/example_netconf.c b/example/example_netconf.c index 5a5710f0..e5f8fb37 100644 --- a/example/example_netconf.c +++ b/example/example_netconf.c @@ -115,7 +115,8 @@ clixon_plugin_init(clicon_handle h) { clicon_debug(1, "%s restconf", __FUNCTION__); /* Register local netconf rpc client (note not backend rpc client) */ - if (rpc_callback_register(h, netconf_client_rpc, NULL, "client-rpc") < 0) + if (rpc_callback_register(h, netconf_client_rpc, NULL, + "urn:example:clixon", "client-rpc") < 0) return NULL; return &api; } diff --git a/example/example_restconf.c b/example/example_restconf.c index e6e395c5..c79d45f1 100644 --- a/example/example_restconf.c +++ b/example/example_restconf.c @@ -344,7 +344,7 @@ clixon_plugin_init(clicon_handle h) { clicon_debug(1, "%s restconf", __FUNCTION__); /* Register local netconf rpc client (note not backend rpc client) */ - if (rpc_callback_register(h, restconf_client_rpc, NULL, "client-rpc") < 0) + if (rpc_callback_register(h, restconf_client_rpc, NULL, "urn:example:clixon", "client-rpc") < 0) return NULL; return &api; } diff --git a/lib/clixon/clixon_netconf_lib.h b/lib/clixon/clixon_netconf_lib.h index 2bb4e8f2..81f454a0 100644 --- a/lib/clixon/clixon_netconf_lib.h +++ b/lib/clixon/clixon_netconf_lib.h @@ -68,5 +68,6 @@ int netconf_malformed_message(cbuf *cb, char *message); int netconf_malformed_message_xml(cxobj **xret, char *message); int netconf_trymerge(cxobj *x, yang_spec *yspec, cxobj **xret); int netconf_module_load(clicon_handle h); +char *netconf_db_find(cxobj *xn, char *name); #endif /* _CLIXON_NETCONF_LIB_H */ diff --git a/lib/clixon/clixon_plugin.h b/lib/clixon/clixon_plugin.h index f83ac143..588ff23b 100644 --- a/lib/clixon/clixon_plugin.h +++ b/lib/clixon/clixon_plugin.h @@ -56,6 +56,8 @@ * @param[out] cbret Return xml tree, eg ..., ietf-netconf:xpath", yspec, &xc) < 0) goto done; - +#ifdef NYI + if (xml_parse_string("ietf-netconf:confirmed-commit", yspec, &xc) < 0) + goto done; +#endif /* Load yang spec */ if (yang_spec_parse_module(h, "ietf-netconf", NULL, yspec)< 0) goto done; @@ -1044,3 +1047,35 @@ netconf_module_load(clicon_handle h) done: return retval; } + +/*! Find some sub-child in netconf/xm request. + * Actually, find a child with a certain name and return its body + * @param[in] xn + * @param[in] name + * @retval db Name of database + * @retval NULL Not found + * The following code returns "source" + * @code + * cxobj *xt = NULL; + * char *db; + * xml_parse_string("source", NULL, &xt); + * db = netconf_db_find(xt, "target"); + * @endcode + */ +char* +netconf_db_find(cxobj *xn, + char *name) +{ + cxobj *xs; /* source */ + cxobj *xi; + char *db = NULL; + + if ((xs = xml_find(xn, name)) == NULL) + goto done; + if ((xi = xml_child_i(xs, 0)) == NULL) + goto done; + db = xml_name(xi); + done: + return db; +} + diff --git a/lib/src/clixon_plugin.c b/lib/src/clixon_plugin.c index ded420cc..5f95bb97 100644 --- a/lib/src/clixon_plugin.c +++ b/lib/src/clixon_plugin.c @@ -404,35 +404,41 @@ clixon_plugin_auth(clicon_handle h, * specification are directly dlsym:ed to the CLI plugin. * It would be possible to use this rpc registering API for CLI plugins as well. * - * @note may have a problem if callbacks are of different types + * When namespace and name match, the callback is made */ typedef struct { qelem_t rc_qelem; /* List header */ clicon_rpc_cb rc_callback; /* RPC Callback */ void *rc_arg; /* Application specific argument to cb */ - char *rc_tag; /* Xml/json tag when matched, callback called */ + char *rc_namespace;/* Namespace to combine with name tag */ + char *rc_name; /* Xml/json tag/name */ } rpc_callback_t; /* List of rpc callback entries */ static rpc_callback_t *rpc_cb_list = NULL; -/*! Register a RPC callback - * Called from plugin to register a callback for a specific netconf XML tag. +/*! Register a RPC callback by appending a new RPC to the list * - * @param[in] h clicon handle - * @param[in] cb, Callback called - * @param[in] arg, Domain-specific argument to send to callback - * @param[in] tag Xml tag when callback is made - * @see rpc_callback_call + * @param[in] h clicon handle + * @param[in] cb, Callback called + * @param[in] arg, Domain-specific argument to send to callback + * @param[in] namespace namespace of rpc + * @param[in] name RPC name + * @see rpc_callback_call which makes the actual callback */ int rpc_callback_register(clicon_handle h, clicon_rpc_cb cb, void *arg, - char *tag) + char *namespace, + char *name) { rpc_callback_t *rc; + if (name == NULL || namespace == NULL){ + clicon_err(OE_DB, EINVAL, "name or namespace NULL"); + goto done; + } if ((rc = malloc(sizeof(rpc_callback_t))) == NULL) { clicon_err(OE_DB, errno, "malloc: %s", strerror(errno)); goto done; @@ -440,13 +446,16 @@ rpc_callback_register(clicon_handle h, memset(rc, 0, sizeof(*rc)); rc->rc_callback = cb; rc->rc_arg = arg; - rc->rc_tag = strdup(tag); /* XXX strdup memleak */ - INSQ(rc, rpc_cb_list); + rc->rc_namespace = strdup(namespace); + rc->rc_name = strdup(name); + ADDQ(rc, rpc_cb_list); return 0; done: if (rc){ - if (rc->rc_tag) - free(rc->rc_tag); + if (rc->rc_namespace) + free(rc->rc_namespace); + if (rc->rc_name) + free(rc->rc_name); free(rc); } return -1; @@ -461,8 +470,10 @@ rpc_callback_delete_all(void) while((rc = rpc_cb_list) != NULL) { DELQ(rc, rpc_cb_list, rpc_callback_t *); - if (rc->rc_tag) - free(rc->rc_tag); + if (rc->rc_namespace) + free(rc->rc_namespace); + if (rc->rc_name) + free(rc->rc_name); free(rc); } return 0; @@ -472,12 +483,15 @@ rpc_callback_delete_all(void) * * @param[in] h clicon handle * @param[in] xn Sub-tree (under xorig) at child of rpc: . - * @param[out] xret Return XML, error or OK + * @param[out] cbret Return XML (as string in CLIgen buffer), error or OK * @param[in] arg Domain-speific arg (eg client_entry) - * * @retval -1 Error * @retval 0 OK, not found handler. - * @retval 1 OK, handler called + * @retval n OK, handler called + * @see rpc_callback_register which register a callback function + * @note that several callbacks can be registered. They need to cooperate on + * return values, ie if one writes cbret, the other needs to handle that by + * leaving it, replacing it or amending it. */ int rpc_callback_call(clicon_handle h, @@ -486,25 +500,32 @@ rpc_callback_call(clicon_handle h, void *arg) { int retval = -1; + int ret; rpc_callback_t *rc; + char *name; + char *prefix; + char *namespace; + int nr = 0; /* How many callbacks */ if (rpc_cb_list == NULL) return 0; rc = rpc_cb_list; do { - if (strcmp(rc->rc_tag, xml_name(xe)) == 0){ - if ((retval = rc->rc_callback(h, xe, cbret, arg, rc->rc_arg)) < 0){ - clicon_debug(1, "%s Error in: %s", __FUNCTION__, rc->rc_tag); - goto done; - } - else{ - retval = 1; /* handled */ + name = xml_name(xe); + prefix = xml_prefix(xe); + xml2ns(xe, prefix, &namespace); + if (strcmp(rc->rc_name, name) == 0 && + namespace && rc->rc_namespace && + strcmp(rc->rc_namespace, namespace) == 0){ + if ((ret = rc->rc_callback(h, xe, cbret, arg, rc->rc_arg)) < 0){ + clicon_debug(1, "%s Error in: %s", __FUNCTION__, rc->rc_name); goto done; } + nr++; } rc = NEXTQ(rpc_callback_t *, rc); } while (rc != rpc_cb_list); - retval = 0; + retval = nr; /* 0: none found, >0 nr of handlers called */ done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); return retval;