clixon/apps/backend/backend_commit.c

1579 lines
54 KiB
C

/*
*
***** BEGIN LICENSE BLOCK *****
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
Copyright (C) 2017-2019 Olof Hagsand
Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate)
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 *****
*/
#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
#endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <pwd.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/param.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netinet/in.h>
/* cligen */
#include <cligen/cligen.h>
/* clicon */
#include <clixon/clixon.h>
#include "clixon_backend_transaction.h"
#include "clixon_backend_plugin.h"
#include "backend_handle.h"
#include "clixon_backend_commit.h"
#include "backend_client.h"
#include "backend_failsafe.h"
/* a global instance of the confirmed_commit struct for reference throughout the procedure */
struct confirmed_commit confirmed_commit = {
.state = INACTIVE,
};
/* flag to carry indication if an RPC bearing <commit/> satisfies conditions to cancel the rollback timer */
static int is_valid_confirming_commit = 0;
/*! Key values are checked for validity independent of user-defined callbacks
*
* Key values are checked as follows:
* 1. If no value and default value defined, add it.
* 2. If no value and mandatory flag set in spec, report error.
* 3. Validate value versus spec, and report error if no match. Currently only int ranges and
* string regexp checked.
* See also db_lv_set() where defaults are also filled in. The case here for defaults
* are if code comes via XML/NETCONF.
* @param[in] yspec Yang spec
* @param[in] td Transaction data
* @param[out] xret Error XML tree. Free with xml_free after use
* @retval -1 Error
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
*/
static int
generic_validate(clicon_handle h,
yang_stmt *yspec,
transaction_data_t *td,
cxobj **xret)
{
int retval = -1;
cxobj *x2;
int i;
int ret;
cbuf *cb = NULL;
/* All entries */
if ((ret = xml_yang_validate_all_top(h, td->td_target, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* changed entries */
for (i=0; i<td->td_clen; i++){
x2 = td->td_tcvec[i]; /* target changed */
/* Should this be recursive? */
if ((ret = xml_yang_validate_add(h, x2, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
/* added entries */
for (i=0; i<td->td_alen; i++){
x2 = td->td_avec[i];
if ((ret = xml_yang_validate_add(h, x2, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
// ok:
retval = 1;
done:
if (cb)
cbuf_free(cb);
return retval;
fail:
retval = 0;
goto done;
}
/*! Common startup validation
* Get db, upgrade it w potential transformed XML, populate it w yang spec,
* sort it, validate it by triggering a transaction
* and call application callback validations.
* @param[in] h Clicon handle
* @param[in] db The startup database. The wanted backend state
* @param[in] td Transaction
* @param[out] cbret CLIgen buffer w error stmt if retval = 0
* @retval -1 Error - or validation failed (but cbret not set)
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
* @note Need to differentiate between error and validation fail
*
* 1. Parse startup XML (or JSON)
* 2. If syntax failure, call startup-cb(ERROR), copy failsafe db to
* candidate and commit. Done
* 3. Check yang module versions between backend and init config XML. (msdiff)
* 4. Validate startup db. (valid)
* 5. If valid fails, call startup-cb(Invalid, msdiff), keep startup in candidate and commit failsafe db. Done.
* 6. Call startup-cb(OK, msdiff) and commit.
* @see validate_common for incoming validate/commit
*/
static int
startup_common(clicon_handle h,
char *db,
transaction_data_t *td,
cbuf *cbret)
{
int retval = -1;
yang_stmt *yspec;
int ret;
modstate_diff_t *msdiff = NULL;
cxobj *xt = NULL;
cxobj *x;
cxobj *xret = NULL;
cxobj *xerr = NULL;
/* If CLICON_XMLDB_MODSTATE is enabled, then get the db XML with
* potentially non-matching module-state in msdiff
*/
if (clicon_option_bool(h, "CLICON_XMLDB_MODSTATE"))
if ((msdiff = modstate_diff_new()) == NULL)
goto done;
clicon_debug(1, "Reading startup config from %s", db);
/* Get the startup datastore WITHOUT binding to YANG, sorting and default setting.
* It is done below, later in this function
*/
if (clicon_option_bool(h, "CLICON_XMLDB_UPGRADE_CHECKOLD")){
if ((ret = xmldb_get0(h, db, YB_MODULE, NULL, "/", 0, &xt, msdiff, &xerr)) < 0)
goto done;
if (ret == 0){ /* ret should not be 0 */
/* Print upgraded db: -q backend switch for debugging/ showing upgraded config only */
if (clicon_quit_upgrade_get(h) == 1){
xml_print(stderr, xerr);
clicon_err(OE_XML, 0, "invalid configuration before upgrade");
exit(0); /* This is fairly abrupt , but need to avoid side-effects of rewinding
* See similar clause below
*/
}
if (clixon_xml2cbuf(cbret, xerr, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
}
else {
if (xmldb_get0(h, db, YB_NONE, NULL, "/", 0, &xt, msdiff, &xerr) < 0)
goto done;
}
if (msdiff && msdiff->md_status == 0){ // Possibly check for CLICON_XMLDB_MODSTATE
clicon_log(LOG_WARNING, "Modstate expected in startup datastore but not found\n"
"This may indicate that the datastore is not initialized corrrectly, such as copy/pasted.\n"
"It may also be normal bootstrapping since module state will be written on next datastore save");
}
if ((yspec = clicon_dbspec_yang(h)) == NULL){
clicon_err(OE_YANG, 0, "Yang spec not set");
goto done;
}
clicon_debug(1, "Reading startup config done");
/* Clear flags xpath for get */
xml_apply0(xt, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset,
(void*)(XML_FLAG_MARK|XML_FLAG_CHANGE));
/* Here xt is old syntax */
/* General purpose datastore upgrade */
if (clixon_plugin_datastore_upgrade_all(h, db, xt, msdiff) < 0)
goto done;
/* Module-specific upgrade callbacks */
if (msdiff){
if ((ret = clixon_module_upgrade(h, xt, msdiff, cbret)) < 0)
goto done;
if (ret == 0)
goto fail;
}
/* Print upgraded db: -q backend switch for debugging/ showing upgraded config only */
if (clicon_quit_upgrade_get(h) == 1){
/* bind yang */
if ((ret = xml_bind_yang(xt, YB_MODULE, yspec, &xret)) < 1){
if (ret == 0){
/* invalid */
clicon_err(OE_XML, EFAULT, "invalid configuration");
}
else {
/* error */
xml_print(stderr, xret);
clicon_err(OE_XML, 0, "%s: YANG binding error", __func__);
}
} /* sort yang */
else if (xml_sort_recurse(xt) < 0) {
clicon_err(OE_XML, EFAULT, "Yang sort error");
}
if (xmldb_dump(h, stdout, xt) < 0)
goto done;
exit(0); /* This is fairly abrupt , but need to avoid side-effects of rewinding
stack. Alternative is to make a separate function stack for this. */
}
/* If empty skip. Note upgrading can add children, so it may be empty before that. */
if (xml_child_nr(xt) == 0){
td->td_target = xt;
xt = NULL;
goto ok;
}
/* After upgrading, XML tree needs to be sorted and yang spec populated */
if ((ret = xml_bind_yang(xt, YB_MODULE, yspec, &xret)) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
/* After upgrade check no state data */
if ((ret = xml_non_config_data(xt, &xret)) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
/* Sort xml */
if (xml_sort_recurse(xt) < 0)
goto done;
/* Add global defaults. */
if (xml_global_defaults(h, xt, NULL, NULL, yspec, 0) < 0)
goto done;
/* Apply default values (removed in clear function) */
if (xml_default_recurse(xt, 0) < 0)
goto done;
/* Handcraft transition with with only add tree */
td->td_target = xt;
xt = NULL;
x = NULL;
while ((x = xml_child_each(td->td_target, x, CX_ELMNT)) != NULL){
xml_flag_set(x, XML_FLAG_ADD); /* Also down */
xml_apply(x, CX_ELMNT, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_ADD);
if (cxvec_append(x, &td->td_avec, &td->td_alen) < 0)
goto done;
}
/* 4. Call plugin transaction start callbacks */
if (plugin_transaction_begin_all(h, td) < 0)
goto done;
/* 5. Make generic validation on all new or changed data.
Note this is only call that uses 3-values */
clicon_debug(1, "Validating startup %s", db);
if ((ret = generic_validate(h, yspec, td, &xret)) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
goto fail; /* STARTUP_INVALID */
}
/* 6. Call plugin transaction validate callbacks */
if (plugin_transaction_validate_all(h, td) < 0)
goto done;
/* 7. Call plugin transaction complete callbacks */
if (plugin_transaction_complete_all(h, td) < 0)
goto done;
ok:
retval = 1;
done:
if (xerr)
xml_free(xerr);
if (xret)
xml_free(xret);
if (xt)
xml_free(xt);
if (msdiff)
modstate_diff_free(msdiff);
return retval;
fail:
retval = 0;
goto done;
}
/*! Read startup db, check upgrades and validate it, return upgraded XML
*
* @param[in] h Clicon handle
* @param[in] db The startup database. The wanted backend state
* @param[out] xtr (Potentially) transformed XML
* @param[out] cbret CLIgen buffer w error stmt if retval = 0
* @retval -1 Error - or validation failed (but cbret not set)
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
*/
int
startup_validate(clicon_handle h,
char *db,
cxobj **xtr,
cbuf *cbret)
{
int retval = -1;
int ret;
transaction_data_t *td = NULL;
/* Handcraft a transition with only target and add trees */
if ((td = transaction_new()) == NULL)
goto done;
if ((ret = startup_common(h, db, td, cbret)) < 0){
plugin_transaction_abort_all(h, td);
goto done;
}
if (ret == 0){
plugin_transaction_abort_all(h, td);
goto fail;
}
plugin_transaction_end_all(h, td);
/* Clear cached trees from default values and marking */
if (xmldb_get0_clear(h, td->td_target) < 0)
goto done;
if (xtr){
*xtr = td->td_target;
td->td_target = NULL;
}
retval = 1;
done:
if (td){
xmldb_get0_free(h, &td->td_target);
transaction_free(td);
}
return retval;
fail: /* cbret should be set */
retval = 0;
goto done;
}
/*! Read startup db, check upgrades and commit it
*
* @param[in] h Clicon handle
* @param[in] db The startup database. The wanted backend state
* @param[out] cbret CLIgen buffer w error stmt if retval = 0
* @retval -1 Error - or validation failed (but cbret not set)
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
* Only called from startup_mode_startup
*/
int
startup_commit(clicon_handle h,
char *db,
cbuf *cbret)
{
int retval = -1;
int ret;
transaction_data_t *td = NULL;
if (strcmp(db,"running")==0){
clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db);
goto done;
}
/* Handcraft a transition with only target and add trees */
if ((td = transaction_new()) == NULL)
goto done;
if ((ret = startup_common(h, db, td, cbret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* 8. Call plugin transaction commit callbacks */
if (plugin_transaction_commit_all(h, td) < 0)
goto done;
/* After commit, make a post-commit call (sure that all plugins have committed) */
if (plugin_transaction_commit_done_all(h, td) < 0)
goto done;
/* Clear cached trees from default values and marking */
if (xmldb_get0_clear(h, td->td_target) < 0)
goto done;
/* [Delete and] create running db */
if (xmldb_exists(h, "running") == 1){
if (xmldb_delete(h, "running") != 0 && errno != ENOENT)
goto done;;
}
if (xmldb_create(h, "running") < 0)
goto done;
/* 9, write (potentially modified) tree to running
* XXX note here startup is copied to candidate, which may confuse everything
* XXX default values are overwritten
*/
if (td->td_target)
/* target is datastore, but is here transformed to mimic an incoming
* edit-config
*/
xml_name_set(td->td_target, NETCONF_INPUT_CONFIG);
if ((ret = xmldb_put(h, "running", OP_REPLACE, td->td_target,
clicon_username_get(h), cbret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* 10. Call plugin transaction end callbacks */
plugin_transaction_end_all(h, td);
retval = 1;
done:
if (td){
if (retval < 1)
plugin_transaction_abort_all(h, td);
xmldb_get0_free(h, &td->td_target);
transaction_free(td);
}
return retval;
fail: /* cbret should be set */
retval = 0;
goto done;
}
/*! Validate a candidate db and comnpare to running
* Get both source and dest datastore, validate target, compute diffs
* and call application callback validations.
* @param[in] h Clicon handle
* @param[in] candidate The candidate database. The wanted backend state
* @param[out] xret Error XML tree, if retval is 0. Free with xml_free after use
* @retval -1 Error - or validation failed (but cbret not set)
* @retval 0 Validation failed (with xret set)
* @retval 1 Validation OK
* @note Need to differentiate between error and validation fail
* (only done for generic_validate)
* @see startup_common for startup scenario
*/
static int
validate_common(clicon_handle h,
char *db,
transaction_data_t *td,
cxobj **xret)
{
int retval = -1;
yang_stmt *yspec;
int i;
cxobj *xn;
int ret;
if ((yspec = clicon_dbspec_yang(h)) == NULL){
clicon_err(OE_FATAL, 0, "No DB_SPEC");
goto done;
}
/* This is the state we are going to */
if ((ret = xmldb_get0(h, db, YB_MODULE, NULL, "/", 0, &td->td_target, NULL, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* Clear flags xpath for get */
xml_apply0(td->td_target, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset,
(void*)(XML_FLAG_MARK|XML_FLAG_CHANGE));
/* 2. Parse xml trees
* This is the state we are going from */
if ((ret = xmldb_get0(h, "running", YB_MODULE, NULL, "/", 0, &td->td_src, NULL, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* Clear flags xpath for get */
xml_apply0(td->td_src, CX_ELMNT, (xml_applyfn_t*)xml_flag_reset,
(void*)(XML_FLAG_MARK|XML_FLAG_CHANGE));
/* 3. Compute differences */
if (xml_diff(yspec,
td->td_src,
td->td_target,
&td->td_dvec, /* removed: only in running */
&td->td_dlen,
&td->td_avec, /* added: only in candidate */
&td->td_alen,
&td->td_scvec, /* changed: original values */
&td->td_tcvec, /* changed: wanted values */
&td->td_clen) < 0)
goto done;
if (clicon_debug_get()>1)
transaction_print(stderr, td);
/* Mark as changed in tree */
for (i=0; i<td->td_dlen; i++){ /* Also down */
xn = td->td_dvec[i];
xml_flag_set(xn, XML_FLAG_DEL);
xml_apply(xn, CX_ELMNT, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_DEL);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
for (i=0; i<td->td_alen; i++){ /* Also down */
xn = td->td_avec[i];
xml_flag_set(xn, XML_FLAG_ADD);
xml_apply(xn, CX_ELMNT, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_ADD);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
for (i=0; i<td->td_clen; i++){ /* Also up */
xn = td->td_scvec[i];
xml_flag_set(xn, XML_FLAG_CHANGE);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
xn = td->td_tcvec[i];
xml_flag_set(xn, XML_FLAG_CHANGE);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
/* 4. Call plugin transaction start callbacks */
if (plugin_transaction_begin_all(h, td) < 0)
goto done;
/* 5. Make generic validation on all new or changed data.
Note this is only call that uses 3-values */
if ((ret = generic_validate(h, yspec, td, xret)) < 0)
goto done;
if (ret == 0)
goto fail;
/* 6. Call plugin transaction validate callbacks */
if (plugin_transaction_validate_all(h, td) < 0)
goto done;
/* 7. Call plugin transaction complete callbacks */
if (plugin_transaction_complete_all(h, td) < 0)
goto done;
retval = 1;
done:
return retval;
fail:
retval = 0;
goto done;
}
/*! Start a validate transaction
*
* @param[in] h Clicon handle
* @param[in] db A candidate database, typically "candidate" but not necessarily so
* @retval -1 Error - or validation failed
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
*/
int
candidate_validate(clicon_handle h,
char *db,
cbuf *cbret)
{
int retval = -1;
transaction_data_t *td = NULL;
cxobj *xret = NULL;
int ret;
clicon_debug(1, "%s", __FUNCTION__);
if (db == NULL || cbret == NULL){
clicon_err(OE_CFG, EINVAL, "db or cbret is NULL");
goto done;
}
/* 1. Start transaction */
if ((td = transaction_new()) == NULL)
goto done;
/* Common steps (with commit) */
if ((ret = validate_common(h, db, td, &xret)) < 0){
/* A little complex due to several sources of validation fails or errors.
* (1) xerr is set -> translate to cbret; (2) cbret set use that; otherwise
* use clicon_err.
* TODO: -1 return should be fatal error, not failed validation
*/
if (!cbuf_len(cbret) &&
netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
goto done;
goto fail;
}
if (ret == 0){
if (xret == NULL){
clicon_err(OE_CFG, EINVAL, "xret is NULL");
goto done;
}
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
if (!cbuf_len(cbret) &&
netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
goto done;
goto fail;
}
if (xmldb_get0_clear(h, td->td_src) < 0 ||
xmldb_get0_clear(h, td->td_target) < 0)
goto done;
plugin_transaction_end_all(h, td);
retval = 1;
done:
if (xret)
xml_free(xret);
if (td){
if (retval < 1)
plugin_transaction_abort_all(h, td);
xmldb_get0_free(h, &td->td_target);
xmldb_get0_free(h, &td->td_src);
transaction_free(td);
}
return retval;
fail:
retval = 0;
goto done;
}
/*! Cancel a scheduled rollback as previously registered by schedule_rollback_event()
*
* @retval 0 Rollback event successfully cancelled
* @retval -1 No Rollback event was found
*/
int
cancel_rollback_event()
{
int retval;
if ((retval = clixon_event_unreg_timeout(confirmed_commit.fn, confirmed_commit.arg)) == 0) {
clicon_log(LOG_INFO, "a scheduled rollback event has been cancelled");
} else {
clicon_log(LOG_WARNING, "the specified scheduled rollback event was not found");
}
return retval;
}
/*! Apply the rollback configuration upon expiration of the confirm-timeout
*
* @param[in] fd a dummy argument per the event callback semantics
* @param[in] arg a void pointer to a clicon_handle
* @retval 0 the rollback was successful
* @retval -1 the rollback failed
* @see do_rollback()
*/
static int
rollback_fn(int fd,
void *arg)
{
clicon_handle h = arg;
clicon_log(LOG_EMERG, "a confirming-commit was not received before the confirm-timeout expired; rolling back");
return do_rollback(h, NULL);
}
/*! Schedule a rollback in case no confirming-commit is received before the confirm-timeout
*
* @param[in] h a clicon handle
* @param[in] timeout a uint32 representing the number of seconds before the rollback event should fire
*
* @retval 0 Rollback event successfully scheduled
* @retval -1 Rollback event was not scheduled
*/
static int
schedule_rollback_event(clicon_handle h,
uint32_t timeout)
{
int retval = -1;
// register a new scheduled event
struct timeval t, t1;
if (gettimeofday(&t, NULL) < 0) {
clicon_err(OE_UNIX, 0, "failed to get time of day: %s", strerror(errno));
goto done;
};
t1.tv_sec = timeout; t1.tv_usec = 0;
timeradd(&t, &t1, &t);
/* The confirmed-commit is either:
* - ephemeral, and the client requesting the new confirmed-commit is on the same session, OR
* - persistent, and the client provided the persist-id in the new confirmed-commit
*/
/* remember the function pointer and args so the confirming-commit can cancel the rollback */
confirmed_commit.fn = rollback_fn;
confirmed_commit.arg = h;
if (clixon_event_reg_timeout(t, rollback_fn, h, "rollback after timeout") < 0) {
/* error is logged in called function */
goto done;
};
retval = 0;
done:
return retval;
}
/*! Handle the second phase of confirmed-commit processing.
*
* In the first phase, the proper action was taken in the case of a valid confirming-commit, but no subsequent
* confirmed-commit.
*
* In the second phase, the action taken is to handle both confirming- and confirmed-commit by creating the
* rollback database as required, then deleting it once the sequence is complete.
*
* @param[in] h Clicon handle
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @retval 0 OK
* @retval -1 Error
*/
static int
handle_confirmed_commit(clicon_handle h,
cbuf *cbret)
{
cxobj *persist_xml;
char *persist;
cxobj *confirm_timeout_xml;
char *confirm_timeout_str;
unsigned long confirm_timeout = 0L;
int retval = -1;
/* The case of a valid confirming-commit is also handled in the first phase, but only if there is no subsequent
* confirmed-commit. It is tested again here as the case of a valid confirming-commit *with* a subsequent
* confirmed-commit must be handled once the transaction has begun and after all the plugins' validate callbacks
* have been called.
*/
if (is_valid_confirming_commit) {
if (cancel_rollback_event() < 0) {
clicon_err(OE_DAEMON, 0, "A valid confirming-commit was received, but the corresponding rollback event was not found");
}
if (confirmed_commit.state == PERSISTENT && confirmed_commit.persist_id != NULL) {
free(confirmed_commit.persist_id);
confirmed_commit.persist_id = NULL;
}
confirmed_commit.state = INACTIVE;
}
/* Now, determine if there is a subsequent confirmed-commit */
if (xml_find_type(confirmed_commit.xe, NULL, "confirmed", CX_ELMNT) != NULL) {
/* There is, get it's confirm-timeout value, which will default per the yang schema if not client-specified */
/* Clixon also pre-validates input according to the schema, so bounds checking here is redundant */
if ((confirm_timeout_xml = xml_find_type(confirmed_commit.xe, NULL, "confirm-timeout", CX_ELMNT)) != NULL) {
if ((confirm_timeout_str = xml_body(confirm_timeout_xml)) == NULL) {
clicon_err(OE_DAEMON, 0, "%s: schema compliance error", __FUNCTION__);
goto done;
};
confirm_timeout = strtoul(confirm_timeout_str, NULL, 10);
}
if ((persist_xml = xml_find_type(confirmed_commit.xe, NULL, "persist", CX_ELMNT)) != NULL) {
/* an empty string is permitted, but it is represeted as NULL */
persist = xml_body(persist_xml);
if (persist == NULL) {
confirmed_commit.persist_id = NULL;
} else if ((confirmed_commit.persist_id = strdup4(persist)) == NULL) {
clicon_err(OE_UNIX, errno, "strdup4");
goto done;
}
/* The client has passed <persist>; the confirming-commit MUST now be accompanied by a matching
* <persist-id>
*/
confirmed_commit.state = PERSISTENT;
clicon_log(LOG_INFO,
"a persistent confirmed-commit has been requested with persist id of '%s' and a timeout of %lu seconds",
confirmed_commit.persist_id, confirm_timeout);
} else {
/* The client did not pass a value for <persist> and therefore any subsequent confirming-commit must be
* issued within the same session.
*/
if (clicon_session_id_get(h, &confirmed_commit.session_id) < 0) {
clicon_err(OE_DAEMON, 0,
"an ephemeral confirmed-commit was issued, but the session-id could not be determined");
if (netconf_operation_failed(cbret, "application",
"there was an error while performing the confirmed-commit") < 0)
clicon_err(OE_DAEMON, 0, "there was an error sending a netconf response to the client");
goto done;
};
confirmed_commit.state = EPHEMERAL;
clicon_log(LOG_INFO,
"an ephemeral confirmed-commit has been requested by session-id %u and a timeout of %lu seconds",
confirmed_commit.session_id, confirm_timeout);
}
/* The confirmed-commits and confirming-commits can overlap; the rollback database is created at the beginning
* of such a sequence and deleted at the end; hence its absence implies this is the first of a sequence. **
*
*
* | edit
* | | confirmed-commit
* | | copy t=0 running to rollback
* | | | edit
* | | | | both
* | | | | | edit
* | | | | | | both
* | | | | | | | confirming-commit
* | | | | | | | | delete rollback
* +----|-|-|-|-|-|-|-|---------------
* t=0 1 2 3 4 5 6 7 8
*
* edit = edit of the candidate configuration
* both = both a confirmed-commit and confirming-commit in the same RPC
*
* As shown, the rollback database created at t=2 is comprised of the running database from t=0
* Thus, if there is a rollback event at t=7, the t=0 configuration will be committed.
*
* ** the rollback database may be present at system startup if there was a crash during a confirmed-commit;
* in the case the system is configured to startup from running and the rollback database is present, the
* rollback database will be committed to running and then deleted. If the system is configured to use a
* startup configuration instead, any present rollback database will be deleted.
*
*/
int db_exists = xmldb_exists(h, "rollback");
if (db_exists == -1) {
clicon_err(OE_DAEMON, 0, "there was an error while checking existence of the rollback database");
goto done;
} else if (db_exists == 0) {
// db does not yet exists
if (xmldb_copy(h, "running", "rollback") < 0) {
clicon_err(OE_DAEMON, 0, "there was an error while copying the running configuration to rollback database.");
goto done;
};
}
if (schedule_rollback_event(h, confirm_timeout) < 0) {
clicon_err(OE_DAEMON, 0, "the rollback event could not be scheduled");
goto done;
};
} else {
/* There was no subsequent confirmed-commit, meaning this is the end of the confirmed/confirming sequence;
* The new configuration is already committed to running and the rollback database can now be deleted
*/
if (xmldb_delete(h, "rollback") < 0) {
clicon_err(OE_DB, 0, "Error deleting the rollback configuration");
goto done;
}
}
retval = 0;
done:
return retval;
}
/*! Do a diff between candidate and running, then start a commit transaction
*
* The code reverts changes if the commit fails. But if the revert
* fails, we just ignore the errors and proceed. Maybe we should
* do something more drastic?
* @param[in] h Clicon handle
* @param[in] db A candidate database, not necessarily "candidate"
* @retval -1 Error - or validation failed
* @retval 0 Validation failed (with cbret set)
* @retval 1 Validation OK
*/
int
candidate_commit(clicon_handle h,
char *db,
cbuf *cbret)
{
int retval = -1;
transaction_data_t *td = NULL;
int ret;
cxobj *xret = NULL;
yang_stmt *yspec;
/* 1. Start transaction */
if ((td = transaction_new()) == NULL)
goto done;
/* Common steps (with validate). Load candidate and running and compute diffs
* Note this is only call that uses 3-values
*/
if ((ret = validate_common(h, db, td, &xret)) < 0)
goto done;
/* If the confirmed-commit feature is enabled, execute phase 2:
* - If a valid confirming-commit, cancel the rollback event
* - If a new confirmed-commit, schedule a new rollback event, otherwise
* - delete the rollback database
*
* Unless, however, this invocation of candidate_commit() was by way of a
* rollback event, in which case the timers are already cancelled and the
* caller will cleanup the rollback database. All that must be done here is
* to activate it.
*/
if ((yspec = clicon_dbspec_yang(h)) == NULL) {
clicon_err(OE_YANG, ENOENT, "No yang spec");
goto done;
}
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")
&& confirmed_commit.state != ROLLBACK
&& handle_confirmed_commit(h, cbret) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
/* 7. Call plugin transaction commit callbacks */
if (plugin_transaction_commit_all(h, td) < 0)
goto done;
/* After commit, make a post-commit call (sure that all plugins have committed) */
if (plugin_transaction_commit_done_all(h, td) < 0)
goto done;
/* Clear cached trees from default values and marking */
if (xmldb_get0_clear(h, td->td_target) < 0)
goto done;
if (xmldb_get0_clear(h, td->td_src) < 0)
goto done;
/* 8. Success: Copy candidate to running
*/
if (xmldb_copy(h, db, "running") < 0)
goto done;
xmldb_modified_set(h, db, 0); /* reset dirty bit */
/* Here pointers to old (source) tree are obsolete */
if (td->td_dvec){
td->td_dlen = 0;
free(td->td_dvec);
td->td_dvec = NULL;
}
if (td->td_scvec){
free(td->td_scvec);
td->td_scvec = NULL;
}
/* 9. Call plugin transaction end callbacks */
plugin_transaction_end_all(h, td);
retval = 1;
done:
/* In case of failure (or error), call plugin transaction termination callbacks */
if (td){
if (retval < 1)
plugin_transaction_abort_all(h, td);
xmldb_get0_free(h, &td->td_target);
xmldb_get0_free(h, &td->td_src);
transaction_free(td);
}
if (xret)
xml_free(xret);
return retval;
fail:
retval = 0;
goto done;
}
/*! Do a rollback of the running configuration to the state prior to initiation of a confirmed-commit
*
* The "running" configuration prior to the first confirmed-commit was stored in another database named "rollback".
* Here, it is committed as if it is the candidate configuration.
*
* @param[in] h Clicon handle
* @retval -1 Error
* @retval 0 Success
* @see backend_client_rm()
* @see from_client_cancel_commit()
* @see rollback_fn()
*/
int
do_rollback(clicon_handle h, uint8_t *errs)
{
/* Execution has arrived here because do_rollback() was called by one of:
* 1. backend_client_rm() (client disconnected and confirmed-commit is ephemeral)
* 2. from_client_cancel_commit() (invoked either by netconf client, or CLI)
* 3. rollback_fn() (invoked by expiration of the rollback event timer)
*/
uint8_t errstate = 0;
int res = -1;
cbuf *cbret;
if ((cbret = cbuf_new()) == NULL) {
clicon_err(OE_DAEMON, 0, "rollback was not performed. (cbuf_new: %s)", strerror(errno));
/* the rollback_db won't be deleted, so one can try recovery by:
* load rollback running
* restart the backend, which will try to load the rollback_db, and delete it if successful
* (otherwise it will load the failsafe)
*/
clicon_log(LOG_EMERG, "An error occurred during rollback and the rollback_db wasn't deleted.");
errstate |= ROLLBACK_NOT_APPLIED | ROLLBACK_DB_NOT_DELETED;
goto done;
}
if (confirmed_commit.state == PERSISTENT && confirmed_commit.persist_id != NULL) {
free(confirmed_commit.persist_id);
confirmed_commit.persist_id = NULL;
}
confirmed_commit.state = ROLLBACK;
if (candidate_commit(h, "rollback", cbret) < 0) { /* Assume validation fail, nofatal */
/* theoretically, this should never error, since the rollback database was previously active and therefore
* had itself been previously and successfully committed.
*/
clicon_log(LOG_CRIT, "An error occurred committing the rollback database.");
errstate |= ROLLBACK_NOT_APPLIED;
/* Rename the errored rollback database */
if (xmldb_rename(h, "rollback", NULL, ".error") < 0) {
clicon_log(LOG_CRIT, "An error occurred renaming the rollback database.");
errstate |= ROLLBACK_DB_NOT_DELETED;
}
/* Attempt to load the failsafe config */
if (load_failsafe(h, "Rollback") < 0) {
clicon_log(LOG_EMERG, "An error occurred committing the failsafe database. Exiting.");
/* Invoke our own signal handler to exit */
raise(SIGINT);
/* should never make it here */
}
errstate |= ROLLBACK_FAILSAFE_APPLIED;
goto done;
}
cbuf_free(cbret);
if (xmldb_delete(h, "rollback") < 0) {
clicon_log(LOG_WARNING, "A rollback occurred but the rollback_db wasn't deleted.");
errstate |= ROLLBACK_DB_NOT_DELETED;
goto done;
};
res = 0;
done:
confirmed_commit.state = INACTIVE;
if (errs)
*errs = errstate;
return res;
}
/*! Determine if the present commit RPC invocation constitutes a valid "confirming-commit".
*
* To be considered a valid confirming-commit, the <commit/> must either:
* 1) be presented without a <persist-id> value, and on the same session as a prior confirmed-commit that itself was
* without a <persist> value, OR
* 2) be presented with a <persist-id> value that matches the <persist> value accompanying the prior confirmed-commit
*
* @param[in] h Clicon handle
* @param[in] myid current client session-id
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @retval 0 The confirming-commit is not valid
* @retval 1 The confirming-commit is valid
* @retval -1 Error
*/
static int
check_valid_confirming_commit(clicon_handle h,
uint32_t myid,
cbuf *cbret)
{
int retval = -1;
cxobj *persist_id_xml = NULL;
char *persist_id = NULL;
if (confirmed_commit.xe == NULL) {
retval = 0;
goto done;
}
switch (confirmed_commit.state) {
case PERSISTENT:
if ((persist_id_xml = xml_find_type(confirmed_commit.xe, NULL, "persist-id", CX_ELMNT)) != NULL) {
persist_id = xml_body(persist_id_xml);
if ((persist_id == NULL && confirmed_commit.persist_id == NULL) || // empty strings deserialized as NULL
(persist_id != NULL && confirmed_commit.persist_id != NULL &&
strcmp(persist_id, confirmed_commit.persist_id) == 0)) {
/* the RPC included a <persist-id> matching the prior confirming-commit's <persist> */
retval = 1;
break;
} else {
netconf_invalid_value(cbret, "protocol", "No such persist-id");
clicon_log(LOG_INFO,
"a persistent confirmed-commit is in progress but the client issued a "
"confirming-commit with an incorrect persist-id");
retval = 0;
break;
}
} else {
netconf_invalid_value(cbret, "protocol", "Persist-id not given");
clicon_log(LOG_INFO,
"a persistent confirmed-commit is in progress but the client issued a confirming-commit"
"without a persist-id");
retval = 0;
break;
}
case EPHEMERAL:
if (myid == confirmed_commit.session_id) {
/* the RPC lacked a <persist-id>, the prior confirming-commit lacked <persist>, and both were issued
* on the same session.
*/
retval = 1;
break;
}
if (netconf_invalid_value(cbret, "protocol","confirming-commit not performed on originating session") < 0) {
clicon_err(OE_NETCONF, 0, "error sending response");
break;
};
clicon_log(LOG_DEBUG, "an ephemeral confirmed-commit is in progress, but there confirming-commit was"
"not issued on the same session as the confirmed-commit");
retval = 0;
break;
default:
clicon_debug(1, "commit-confirmed state !? %d", confirmed_commit.state);
retval = 0;
break;
}
done:
return retval;
}
/*! Commit the candidate configuration as the device's new current configuration
*
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @param[in] arg client-entry
* @param[in] regarg User argument given at rpc_callback_register()
* @retval 0 OK
* @retval -1 Error
* @note NACM: The server MUST determine the exact nodes in the running
* configuration datastore that are actually different and only check
* "create", "update", and "delete" access permissions for this set of
* nodes, which could be empty.
*/
int
from_client_commit(clicon_handle h,
cxobj *xe,
cbuf *cbret,
void *arg,
void *regarg)
{
int retval = -1;
struct client_entry *ce = (struct client_entry *)arg;
uint32_t myid = ce->ce_id;
uint32_t iddb;
cbuf *cbx = NULL; /* Assist cbuf */
int ret;
yang_stmt *yspec;
/* Handle the first phase of confirmed-commit
*
* First, it must be determined if the given <commit> RPC constitutes a "confirming-commit", roughly meaning:
* 1) it was issued in the same session as a prior confirmed-commit
* 2) it bears a <persist-id> element matching the <persist> element that accompanied the prior confirmed-commit
*
* If it is a valid "confirming-commit" and this RPC does not bear another <confirmed/> element, then the
* confirmed-commit is complete, the rollback event can be cancelled and the rollback database deleted.
*
* No further action is necessary as the candidate configuration was already copied to the running configuration.
*
* If the RPC does bear another <confirmed/> element, that will be handled in phase two, from within the
* candidate_commit() method.
*/
if ((yspec = clicon_dbspec_yang(h)) == NULL) {
clicon_err(OE_YANG, ENOENT, "No yang spec");
goto done;
}
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
confirmed_commit.xe = xe;
if ((is_valid_confirming_commit = check_valid_confirming_commit(h, myid, cbret)) < 0)
goto done;
/* If <confirmed/> is *not* present, this will conclude the confirmed-commit, so cancel the rollback. */
if (xml_find_type(confirmed_commit.xe, NULL, "confirmed", CX_ELMNT) == NULL
&& is_valid_confirming_commit) {
cancel_rollback_event();
if (confirmed_commit.state == PERSISTENT && confirmed_commit.persist_id != NULL) {
free(confirmed_commit.persist_id);
confirmed_commit.persist_id = NULL;
}
confirmed_commit.state = INACTIVE;
if (xmldb_delete(h, "rollback") < 0)
clicon_err(OE_DB, 0, "Error deleting the rollback configuration");
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
goto ok;
}
}
/* Check if target locked by other client */
iddb = xmldb_islocked(h, "running");
if (iddb && myid != iddb){
if ((cbx = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
cprintf(cbx, "<session-id>%u</session-id>", iddb);
if (netconf_in_use(cbret, cbuf_get(cbx), "Operation failed, lock is already held") < 0)
goto done;
goto ok;
}
if ((ret = candidate_commit(h, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
clicon_debug(1, "Commit candidate failed");
if (ret < 0)
if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
goto done;
goto ok;
}
if (ret == 1)
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
ok:
retval = 0;
done:
confirmed_commit.xe = NULL;
if (cbx)
cbuf_free(cbx);
return retval; /* may be zero if we ignoring errors from commit */
} /* from_client_commit */
/*! Revert the candidate configuration to the current running configuration.
*
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @param[in] arg client-entry
* @param[in] regarg User argument given at rpc_callback_register()
* @retval 0 OK. This may indicate both ok and err msg back to client
* @retval 0 OK
* @retval -1 Error
* NACM: No datastore permissions are needed.
*/
int
from_client_discard_changes(clicon_handle h,
cxobj *xe,
cbuf *cbret,
void *arg,
void *regarg)
{
int retval = -1;
struct client_entry *ce = (struct client_entry *)arg;
uint32_t myid = ce->ce_id;
uint32_t iddb;
cbuf *cbx = NULL; /* Assist cbuf */
/* Check if target locked by other client */
iddb = xmldb_islocked(h, "candidate");
if (iddb && myid != iddb){
if ((cbx = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
cprintf(cbx, "<session-id>%u</session-id>", iddb);
if (netconf_lock_denied(cbret, cbuf_get(cbx), "Operation failed, lock is already held") < 0)
goto done;
goto ok;
}
if (xmldb_copy(h, "running", "candidate") < 0){
if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
goto done;
goto ok;
}
xmldb_modified_set(h, "candidate", 0); /* reset dirty bit */
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
ok:
retval = 0;
done:
if (cbx)
cbuf_free(cbx);
return retval; /* may be zero if we ignoring errors from commit */
}
/*! 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.
* If the confirmed-commit is ephemeral, the 'persist-id' must not be given and both the confirmed-commit and the
* cancel-commit must originate from the same session.
*
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @param[in] arg client-entry
* @param[in] regarg User argument given at rpc_callback_register()
* @retval 0 OK. This may indicate both ok and err msg back to client
* @retval 0 OK
* @retval -1 Error
* @see RFC 6241 Sec 8.4
*/
int
from_client_cancel_commit(clicon_handle h,
cxobj *xe,
cbuf *cbret,
void *arg,
void *regarg)
{
cxobj *persist_id_xml;
char *persist_id = NULL;
uint32_t cur_session_id;
int retval = -1;
if ((persist_id_xml = xml_find_type(xe, NULL, "persist-id", CX_ELMNT)) != NULL) {
/* persist == persist_id == NULL is legal */
persist_id = xml_body(persist_id_xml);
}
switch(confirmed_commit.state) {
case EPHEMERAL:
if (persist_id_xml != NULL) {
if (netconf_invalid_value(cbret, "protocol", "current confirmed-commit is not persistent") < 0)
goto netconf_response_error;
goto done;
}
if (clicon_session_id_get(h, &cur_session_id) < 0) {
if (netconf_invalid_value(cbret, "application", "session-id was not set") < 0)
goto netconf_response_error;
goto done;
}
if (cur_session_id != confirmed_commit.session_id) {
if (netconf_invalid_value(cbret, "protocol", "confirming-commit must be given within session that gave the confirmed-commit") < 0)
goto netconf_response_error;
goto done;
}
goto rollback;
case PERSISTENT:
if (persist_id_xml == NULL) {
if (netconf_invalid_value(cbret, "protocol", "persist-id is required") < 0)
goto netconf_response_error;
goto done;
}
if (persist_id == confirmed_commit.persist_id ||
(persist_id != NULL && confirmed_commit.persist_id != NULL
&& strcmp(persist_id, confirmed_commit.persist_id) == 0)) {
goto rollback;
}
if (netconf_invalid_value(cbret, "application", "a confirmed-commit with the given persist-id was not found") < 0)
goto netconf_response_error;
goto done;
case INACTIVE:
if (netconf_invalid_value(cbret, "application", "no confirmed-commit is in progress") < 0)
goto netconf_response_error;
goto done;
default:
clicon_err(OE_DAEMON, 0, "Unhandled confirmed-commit state");
if (netconf_invalid_value(cbret, "application", "server error") < 0)
goto netconf_response_error;
goto done;
}
/* all invalid conditions jump to done: and valid code paths jump to or fall through to here. */
rollback:
cancel_rollback_event();
if ((retval = do_rollback(h, NULL)) < 0) {
if (netconf_operation_failed(cbret, "application", "rollback failed") < 0)
goto netconf_response_error;
} else {
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
retval = 0;
clicon_log(LOG_INFO, "a confirmed-commit has been cancelled by client request");
}
goto done;
netconf_response_error:
clicon_err(OE_DAEMON, 0, "failed to write netconf response");
done:
return retval;
}
/*! Validates the contents of the specified configuration.
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @param[in] arg client-entry
* @param[in] regarg User argument given at rpc_callback_register()
* @retval 0 OK. This may indicate both ok and err msg back to client
* (eg invalid)
* @retval -1 Error
*/
int
from_client_validate(clicon_handle h,
cxobj *xe,
cbuf *cbret,
void *arg,
void *regarg)
{
int retval = -1;
int ret;
char *db;
clicon_debug(1, "%s", __FUNCTION__);
if ((db = netconf_db_find(xe, "source")) == NULL){
if (netconf_missing_element(cbret, "protocol", "source", NULL) < 0)
goto done;
goto ok;
}
if ((ret = candidate_validate(h, db, cbret)) < 0)
goto done;
if (ret == 1)
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
ok:
retval = 0;
done:
return retval;
} /* from_client_validate */
/*! Restart specific backend plugins without full backend restart
* Note, depending on plugin callbacks, there may be other dependencies which may make this
* difficult in the general case.
*/
int
from_client_restart_one(clicon_handle h,
clixon_plugin_t *cp,
cbuf *cbret)
{
int retval = -1;
char *db = "tmp";
transaction_data_t *td = NULL;
plgreset_t *resetfn; /* Plugin auth */
int ret;
cxobj *xerr = NULL;
yang_stmt *yspec;
int i;
cxobj *xn;
void *wh = NULL;
yspec = clicon_dbspec_yang(h);
if (xmldb_db_reset(h, db) < 0)
goto done;
/* Application may define extra xml in its reset function*/
if ((resetfn = clixon_plugin_api_get(cp)->ca_reset) != NULL){
wh = NULL;
if (plugin_context_check(h, &wh, clixon_plugin_name_get(cp), __FUNCTION__) < 0)
goto done;
if ((retval = resetfn(h, db)) < 0) {
clicon_debug(1, "plugin_start() failed");
goto done;
}
if (plugin_context_check(h, &wh, clixon_plugin_name_get(cp), __FUNCTION__) < 0)
goto done;
}
/* 1. Start transaction */
if ((td = transaction_new()) == NULL)
goto done;
/* This is the state we are going to */
if (xmldb_get0(h, "running", YB_MODULE, NULL, "/", 0, &td->td_target, NULL, NULL) < 0)
goto done;
if ((ret = xml_yang_validate_all_top(h, td->td_target, &xerr)) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xerr, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
/* This is the state we are going from */
if (xmldb_get0(h, db, YB_MODULE, NULL, "/", 0, &td->td_src, NULL, NULL) < 0)
goto done;
/* 3. Compute differences */
if (xml_diff(yspec,
td->td_src,
td->td_target,
&td->td_dvec, /* removed: only in running */
&td->td_dlen,
&td->td_avec, /* added: only in candidate */
&td->td_alen,
&td->td_scvec, /* changed: original values */
&td->td_tcvec, /* changed: wanted values */
&td->td_clen) < 0)
goto done;
/* Mark as changed in tree */
for (i=0; i<td->td_dlen; i++){ /* Also down */
xn = td->td_dvec[i];
xml_flag_set(xn, XML_FLAG_DEL);
xml_apply(xn, CX_ELMNT, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_DEL);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
for (i=0; i<td->td_alen; i++){ /* Also down */
xn = td->td_avec[i];
xml_flag_set(xn, XML_FLAG_ADD);
xml_apply(xn, CX_ELMNT, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_ADD);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
for (i=0; i<td->td_clen; i++){ /* Also up */
xn = td->td_scvec[i];
xml_flag_set(xn, XML_FLAG_CHANGE);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
xn = td->td_tcvec[i];
xml_flag_set(xn, XML_FLAG_CHANGE);
xml_apply_ancestor(xn, (xml_applyfn_t*)xml_flag_set, (void*)XML_FLAG_CHANGE);
}
/* Call plugin transaction start callbacks */
if (plugin_transaction_begin_one(cp, h, td) < 0)
goto fail;
/* Make generic validation on all new or changed data.
Note this is only call that uses 3-values */
if ((ret = generic_validate(h, yspec, td, &xerr)) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xerr, 0, 0, -1, 0) < 0)
goto done;
goto fail;
}
/* Call validate callback in this plugin */
if (plugin_transaction_validate_one(cp, h, td) < 0)
goto fail;
if (plugin_transaction_complete_one(cp, h, td) < 0)
goto fail;
/* Call commit callback in this plugin */
if (plugin_transaction_commit_one(cp, h, td) < 0)
goto fail;
if (plugin_transaction_commit_done_one(cp, h, td) < 0)
goto fail;
/* Finalize */
if (plugin_transaction_end_one(cp, h, td) < 0)
goto fail;
retval = 1;
done:
if (td){
xmldb_get0_free(h, &td->td_target);
transaction_free(td);
}
return retval;
fail:
retval = 0;
goto done;
}