Initial implementation of NETCONF confirmed-commit
This commit is contained in:
parent
954e5d56fd
commit
284316b646
16 changed files with 1375 additions and 26 deletions
|
|
@ -87,6 +87,10 @@ INCLUDES = -I. -I$(top_srcdir)/lib/src -I$(top_srcdir)/lib -I$(top_srcdir)/inclu
|
|||
# Name of application
|
||||
APPL = clixon_backend
|
||||
|
||||
# Source / objects called from plugin and otherwise
|
||||
COMMONSRC = backend_failsafe.c
|
||||
COMMONOBJ = $(COMMONSRC:.c=.o)
|
||||
|
||||
# Not accessible from plugin
|
||||
APPSRC = backend_main.c
|
||||
APPSRC += backend_socket.c
|
||||
|
|
@ -94,14 +98,14 @@ APPSRC += backend_client.c
|
|||
APPSRC += backend_get.c
|
||||
APPSRC += backend_plugin_restconf.c # Pseudo plugin for restconf daemon
|
||||
APPSRC += backend_startup.c
|
||||
APPOBJ = $(APPSRC:.c=.o)
|
||||
APPOBJ = $(APPSRC:.c=.o) $(COMMONOBJ)
|
||||
|
||||
# Accessible from plugin
|
||||
LIBSRC = clixon_backend_transaction.c
|
||||
LIBSRC += clixon_backend_handle.c
|
||||
LIBSRC += backend_commit.c
|
||||
LIBSRC += backend_plugin.c
|
||||
LIBOBJ = $(LIBSRC:.c=.o)
|
||||
LIBOBJ = $(LIBSRC:.c=.o) $(COMMONOBJ)
|
||||
|
||||
# Name of lib
|
||||
MYNAME = clixon_backend
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ release_all_dbs(clicon_handle h,
|
|||
* Finally actually remove client struct in handle
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] ce Client handle
|
||||
* @retval -1 Error (fatal)
|
||||
* @retval 0 Ok
|
||||
* @see backend_client_delete for actual deallocation of client entry struct
|
||||
*/
|
||||
int
|
||||
|
|
@ -168,6 +170,33 @@ backend_client_rm(clicon_handle h,
|
|||
struct client_entry *c;
|
||||
struct client_entry *c0;
|
||||
struct client_entry **ce_prev;
|
||||
uint32_t myid = ce->ce_id;
|
||||
yang_stmt *yspec;
|
||||
int retval = -1;
|
||||
|
||||
/* If the confirmed-commit feature is enabled, rollback any ephemeral commit originated by this client */
|
||||
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")) {
|
||||
if (confirmed_commit.state == EPHEMERAL) {
|
||||
/* See if this client is the origin */
|
||||
clicon_debug(1, "session_id: %u, confirmed_commit.session_id: %u", ce->ce_id, confirmed_commit.session_id);
|
||||
|
||||
if (myid == confirmed_commit.session_id) {
|
||||
clicon_debug(1, "ok, rolling back");
|
||||
clicon_log(LOG_NOTICE, "a client with an active ephemeral confirmed-commit has disconnected; rolling back");
|
||||
|
||||
/* do_rollback errors are logged internally and there is no client to report errors to, so errors are
|
||||
* ignored here.
|
||||
*/
|
||||
cancel_rollback_event();
|
||||
do_rollback(h, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clicon_debug(1, "%s", __FUNCTION__);
|
||||
/* for all streams: XXX better to do it top-level? */
|
||||
|
|
@ -187,7 +216,10 @@ backend_client_rm(clicon_handle h,
|
|||
}
|
||||
ce_prev = &c->ce_next;
|
||||
}
|
||||
return backend_client_delete(h, ce); /* actually purge it */
|
||||
retval = backend_client_delete(h, ce); /* actually purge it */
|
||||
|
||||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Get clixon per datastore stats
|
||||
|
|
@ -423,6 +455,19 @@ from_client_edit_config(clicon_handle h,
|
|||
autocommit = 1;
|
||||
/* If autocommit option is set or requested by client */
|
||||
if (clicon_autocommit(h) || autocommit) {
|
||||
// TODO: if this is from a restconf client ...
|
||||
// and, if there is an existing ephemeral commit, set is_valid_confirming_commit=1 such that
|
||||
// candidate_commit will apply the configuration per RFC 8040 1.4:
|
||||
// If a confirmed commit procedure is
|
||||
// in progress by any NETCONF client, then any new commit will act as
|
||||
// the confirming commit.
|
||||
// and, if there is an existing persistent commit, netconf_operation_failed with "in-use", so
|
||||
// that the restconf server will return "409 Conflict" per RFC 8040 1.4:
|
||||
// If the NETCONF server is expecting a
|
||||
// "persist-id" parameter to complete the confirmed commit procedure,
|
||||
// then the RESTCONF edit operation MUST fail with a "409 Conflict"
|
||||
// status-line. The error-tag "in-use" is used in this case.
|
||||
|
||||
if ((ret = candidate_commit(h, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
|
||||
if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
|
||||
goto done;
|
||||
|
|
|
|||
|
|
@ -70,6 +70,15 @@
|
|||
#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
|
||||
*
|
||||
|
|
@ -634,6 +643,240 @@ candidate_validate(clicon_handle h,
|
|||
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
|
||||
|
|
@ -654,6 +897,7 @@ candidate_commit(clicon_handle h,
|
|||
transaction_data_t *td = NULL;
|
||||
int ret;
|
||||
cxobj *xret = NULL;
|
||||
yang_stmt *yspec;
|
||||
|
||||
/* 1. Start transaction */
|
||||
if ((td = transaction_new()) == NULL)
|
||||
|
|
@ -664,6 +908,27 @@ candidate_commit(clicon_handle h,
|
|||
*/
|
||||
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;
|
||||
|
|
@ -720,6 +985,177 @@ candidate_commit(clicon_handle h,
|
|||
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
|
||||
|
|
@ -747,6 +1183,52 @@ from_client_commit(clicon_handle h,
|
|||
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");
|
||||
|
|
@ -772,6 +1254,7 @@ from_client_commit(clicon_handle h,
|
|||
ok:
|
||||
retval = 0;
|
||||
done:
|
||||
confirmed_commit.xe = NULL;
|
||||
if (cbx)
|
||||
cbuf_free(cbx);
|
||||
return retval; /* may be zero if we ignoring errors from commit */
|
||||
|
|
@ -832,6 +1315,8 @@ from_client_discard_changes(clicon_handle h,
|
|||
/*! 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>
|
||||
|
|
@ -850,9 +1335,85 @@ from_client_cancel_commit(clicon_handle h,
|
|||
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;
|
||||
// done:
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
105
apps/backend/backend_failsafe.c
Normal file
105
apps/backend/backend_failsafe.c
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
|
||||
Copyright (C) 2017-2019 Olof Hagsand
|
||||
Copyright (C) 2020-2021 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 <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <syslog.h>
|
||||
|
||||
/* cligen */
|
||||
#include <cligen/cligen.h>
|
||||
|
||||
/* clixon */
|
||||
#include <clixon/clixon.h>
|
||||
|
||||
#include "clixon_backend_commit.h"
|
||||
#include "backend_failsafe.h"
|
||||
|
||||
/*! Reset running and start in failsafe mode. If no failsafe then quit.
|
||||
Typically done when startup status is not OK so
|
||||
|
||||
failsafe ----------------------+
|
||||
reset \ commit
|
||||
running ----|-------+---------------> RUNNING FAILSAFE
|
||||
\
|
||||
tmp |---------------------->
|
||||
*/
|
||||
int
|
||||
load_failsafe(clicon_handle h, char *phase)
|
||||
{
|
||||
int retval = -1;
|
||||
int ret;
|
||||
char *db = "failsafe";
|
||||
cbuf *cbret = NULL;
|
||||
|
||||
phase = phase == NULL ? "(unknown)" : phase;
|
||||
|
||||
if ((cbret = cbuf_new()) == NULL){
|
||||
clicon_err(OE_XML, errno, "cbuf_new");
|
||||
goto done;
|
||||
}
|
||||
if ((ret = xmldb_exists(h, db)) < 0)
|
||||
goto done;
|
||||
if (ret == 0){ /* No it does not exist, fail */
|
||||
clicon_err(OE_DB, 0, "%s failed and no Failsafe database found, exiting", phase);
|
||||
goto done;
|
||||
}
|
||||
/* Copy original running to tmp as backup (restore if error) */
|
||||
if (xmldb_copy(h, "running", "tmp") < 0)
|
||||
goto done;
|
||||
if (xmldb_db_reset(h, "running") < 0)
|
||||
goto done;
|
||||
ret = candidate_commit(h, db, cbret);
|
||||
if (ret != 1)
|
||||
if (xmldb_copy(h, "tmp", "running") < 0)
|
||||
goto done;
|
||||
if (ret < 0)
|
||||
goto done;
|
||||
if (ret == 0){
|
||||
clicon_err(OE_DB, 0, "%s failed, Failsafe database validation failed %s", phase, cbuf_get(cbret));
|
||||
goto done;
|
||||
}
|
||||
clicon_log(LOG_NOTICE, "%s failed, Failsafe database loaded ", phase);
|
||||
retval = 0;
|
||||
done:
|
||||
if (cbret)
|
||||
cbuf_free(cbret);
|
||||
return retval;
|
||||
}
|
||||
42
apps/backend/backend_failsafe.h
Normal file
42
apps/backend/backend_failsafe.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
|
||||
Copyright (C) 2017-2019 Olof Hagsand
|
||||
Copyright (C) 2020-2021 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 *****
|
||||
|
||||
*/
|
||||
|
||||
#ifndef CLIXON_BACKEND_FAILSAFE_H
|
||||
#define CLIXON_BACKEND_FAILSAFE_H
|
||||
int load_failsafe(clicon_handle h, char *phase);
|
||||
|
||||
#endif //CLIXON_BACKEND_FAILSAFE_H
|
||||
|
|
@ -154,6 +154,10 @@ backend_sig_term(int arg)
|
|||
if (i++ == 0)
|
||||
clicon_log(LOG_NOTICE, "%s: %s: pid: %u Signal %d",
|
||||
__PROGRAM__, __FUNCTION__, getpid(), arg);
|
||||
if (confirmed_commit.persist_id != NULL) {
|
||||
free(confirmed_commit.persist_id);
|
||||
confirmed_commit.persist_id = NULL;
|
||||
}
|
||||
clixon_exit_set(1); /* checked in clixon_event_loop() */
|
||||
}
|
||||
|
||||
|
|
@ -883,6 +887,9 @@ main(int argc,
|
|||
}
|
||||
switch (startup_mode){
|
||||
case SM_INIT: /* Scratch running and start from empty */
|
||||
/* Delete any rollback database, if it exists */
|
||||
// TODO: xmldb_delete doesn't actually unlink; need to look at this
|
||||
xmldb_delete(h, "rollback");
|
||||
/* [Delete and] create running db */
|
||||
if (xmldb_db_reset(h, "running") < 0)
|
||||
goto done;
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ startup_mode_startup(clicon_handle h,
|
|||
{
|
||||
int retval = -1;
|
||||
int ret;
|
||||
int db_exists;
|
||||
|
||||
if (strcmp(db, "running")==0){
|
||||
clicon_err(OE_FATAL, 0, "Invalid startup db: %s", db);
|
||||
|
|
@ -144,7 +145,45 @@ startup_mode_startup(clicon_handle h,
|
|||
if (xmldb_create(h, db) < 0) /* diff */
|
||||
return -1;
|
||||
}
|
||||
if ((ret = startup_commit(h, db, cbret)) < 0)
|
||||
|
||||
/* When a confirming-commit is issued, the confirmed-commit timeout
|
||||
* callback is removed and then the rollback database is deleted.
|
||||
*
|
||||
* The presence of a rollback database means that before the rollback
|
||||
* database was deleted, either clixon_backend crashed or the machine
|
||||
* rebooted.
|
||||
*/
|
||||
yang_stmt *yspec = clicon_dbspec_yang(h);
|
||||
if (if_feature(yspec, "ietf-netconf", "configmed-commit")) {
|
||||
db_exists = xmldb_exists(h, "rollback");
|
||||
if (db_exists < 0) {
|
||||
clicon_err(OE_DAEMON, 0, "Error checking for the existence of the rollback database");
|
||||
goto done;
|
||||
} else if (db_exists == 1) {
|
||||
ret = startup_commit(h, "rollback", cbret);
|
||||
switch(ret) {
|
||||
case -1:
|
||||
case 0:
|
||||
/* validation failed, cbret set */
|
||||
if ((ret = startup_commit(h, "failsafe", cbret)) < 0)
|
||||
goto fail;
|
||||
|
||||
/* Rename the errored rollback database so that it is not tried on a subsequent startup */
|
||||
xmldb_rename(h, db, NULL, ".error");
|
||||
|
||||
retval = 1;
|
||||
goto done;
|
||||
case 1:
|
||||
/* validation ok */
|
||||
retval = 1;
|
||||
xmldb_delete(h, "rollback");
|
||||
goto done;
|
||||
default:
|
||||
/* Unexpected response */
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
} else if ((ret = startup_commit(h, db, cbret)) < 0)
|
||||
goto done;
|
||||
if (ret == 0)
|
||||
goto fail;
|
||||
|
|
|
|||
|
|
@ -39,9 +39,40 @@
|
|||
#ifndef _CLIXON_BACKEND_COMMIT_H_
|
||||
#define _CLIXON_BACKEND_COMMIT_H_
|
||||
|
||||
#define ROLLBACK_NOT_APPLIED 1
|
||||
#define ROLLBACK_DB_NOT_DELETED 2
|
||||
#define ROLLBACK_FAILSAFE_APPLIED 4
|
||||
|
||||
#define COMMIT_NOT_CONFIRMED "Commit was not confirmed; automatic rollback complete."
|
||||
|
||||
enum confirmed_commit_state {
|
||||
INACTIVE, // a confirmed-commit is not in progress
|
||||
PERSISTENT, // a confirmed-commit is in progress and a persist value was given
|
||||
EPHEMERAL, // a confirmed-commit is in progress and a persist value was not given
|
||||
ROLLBACK
|
||||
};
|
||||
|
||||
/* A struct to store the information necessary for tracking the status and relevant details of
|
||||
* one or more overlapping confirmed-commit events.
|
||||
*/
|
||||
struct confirmed_commit {
|
||||
enum confirmed_commit_state state;
|
||||
char *persist_id; // a value given by a client in the confirmed-commit
|
||||
uint32_t session_id; // the session_id of the client that gave no <persist> value
|
||||
|
||||
cxobj *xe; // the commit confirmed request
|
||||
int (*fn)(int, void*); // the function pointer for the rollback event (rollback_fn())
|
||||
void *arg; // the clicon_handle that will be passed to rollback_fn()
|
||||
};
|
||||
|
||||
extern struct confirmed_commit confirmed_commit;
|
||||
|
||||
/*
|
||||
* Prototypes
|
||||
*/
|
||||
int do_rollback(clicon_handle h, uint8_t *errs);
|
||||
int cancel_rollback_event();
|
||||
|
||||
int startup_validate(clicon_handle h, char *db, cxobj **xtr, cbuf *cbret);
|
||||
int startup_commit(clicon_handle h, char *db, cbuf *cbret);
|
||||
int candidate_validate(clicon_handle h, char *db, cbuf *cbret);
|
||||
|
|
|
|||
|
|
@ -648,8 +648,23 @@ cli_commit(clicon_handle h,
|
|||
cvec *argv)
|
||||
{
|
||||
int retval = -1;
|
||||
uint32_t timeout = 0; /* any non-zero value means "confirmed-commit" */
|
||||
cg_var *timeout_var;
|
||||
char *persist = NULL;
|
||||
char *persist_id = NULL;
|
||||
|
||||
if ((retval = clicon_rpc_commit(h)) < 0)
|
||||
int confirmed = (cvec_find_str(vars, "confirmed") != NULL);
|
||||
int cancel = (cvec_find_str(vars, "cancel") != NULL);
|
||||
|
||||
if ((timeout_var = cvec_find(vars, "timeout")) != NULL) {
|
||||
timeout = cv_uint32_get(timeout_var);
|
||||
clicon_debug(1, "commit confirmed with timeout %ul", timeout);
|
||||
}
|
||||
|
||||
persist = cvec_find_str(vars, "persist-val");
|
||||
persist_id = cvec_find_str(vars, "persist-id-val");
|
||||
|
||||
if ((retval = clicon_rpc_commit(h, confirmed, cancel, timeout, persist, persist_id)) < 0)
|
||||
goto done;
|
||||
retval = 0;
|
||||
done:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<clixon-config xmlns="http://clicon.org/config">
|
||||
<CLICON_CONFIGFILE>/usr/local/etc/example.xml</CLICON_CONFIGFILE>
|
||||
<CLICON_FEATURE>ietf-netconf:startup</CLICON_FEATURE>
|
||||
<CLICON_FEATURE>ietf-netconf:confirmed-commit</CLICON_FEATURE>
|
||||
<CLICON_FEATURE>clixon-restconf:allow-auth-none</CLICON_FEATURE>
|
||||
<CLICON_FEATURE>clixon-restconf:fcgi</CLICON_FEATURE>
|
||||
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,15 @@ delete("Delete a configuration item") {
|
|||
all("Delete whole candidate configuration"), delete_all("candidate");
|
||||
}
|
||||
validate("Validate changes"), cli_validate();
|
||||
commit("Commit the changes"), cli_commit();
|
||||
commit("Commit the changes"), cli_commit(); {
|
||||
[persist-id("Specify the 'persist' value of a previous confirmed-commit") <persist-id-val:string show:"string">("The 'persist' value of the persistent confirmed-commit")], cli_commit(); {
|
||||
<cancel:string keyword:cancel>("Cancel an ongoing confirmed-commit"), cli_commit();
|
||||
<confirmed:string keyword:confirmed>("Require a confirming commit") {
|
||||
[persist("Make this confirmed-commit persistent") <persist-val:string show:"string">("The value that must be provided as 'persist-id' in the confirming-commit or cancel-commit")]
|
||||
[<timeout:uint32 range[1:4294967295] show:"1..4294967295">("The rollback timeout in seconds")], cli_commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
quit("Quit"), cli_quit();
|
||||
|
||||
debug("Debugging parts of the system"){
|
||||
|
|
|
|||
|
|
@ -76,5 +76,6 @@ int xmldb_modified_set(clicon_handle h, const char *db, int value);
|
|||
int xmldb_empty_get(clicon_handle h, const char *db);
|
||||
int xmldb_dump(clicon_handle h, FILE *f, cxobj *xt);
|
||||
int xmldb_print(clicon_handle h, FILE *f);
|
||||
int xmldb_rename(clicon_handle h, const char *db, const char *newdb, const char *suffix);
|
||||
|
||||
#endif /* _CLIXON_DATASTORE_H */
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
/*
|
||||
* t
|
||||
*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ int clicon_rpc_get_pageable_list(clicon_handle h, char *datastore, char *xpath,
|
|||
int clicon_rpc_close_session(clicon_handle h);
|
||||
int clicon_rpc_kill_session(clicon_handle h, uint32_t session_id);
|
||||
int clicon_rpc_validate(clicon_handle h, char *db);
|
||||
int clicon_rpc_commit(clicon_handle h);
|
||||
int clicon_rpc_commit(clicon_handle h, int confirmed, int cancel, uint32_t timeout, char *persist, char *persist_id);
|
||||
int clicon_rpc_discard_changes(clicon_handle h);
|
||||
int clicon_rpc_create_subscription(clicon_handle h, char *stream, char *filter, int *s);
|
||||
int clicon_rpc_debug(clicon_handle h, int level);
|
||||
|
|
|
|||
|
|
@ -418,8 +418,16 @@ xmldb_delete(clicon_handle h,
|
|||
if (xmldb_db2file(h, db, &filename) < 0)
|
||||
goto done;
|
||||
if (lstat(filename, &sb) == 0)
|
||||
if (truncate(filename, 0) < 0){
|
||||
clicon_err(OE_DB, errno, "truncate %s", filename);
|
||||
// TODO this had been changed from unlink to truncate some time ago. It was changed back for confirmed-commit
|
||||
// as the presence of the rollback_db at startup triggers loading of the rollback rather than the startup
|
||||
// configuration. It might not be sufficient to check for a truncated file. Needs more review, switching back
|
||||
// to unlink temporarily.
|
||||
// if (truncate(filename, 0) < 0){
|
||||
// clicon_err(OE_DB, errno, "truncate %s", filename);
|
||||
// goto done;
|
||||
// }
|
||||
if (unlink(filename) < 0) {
|
||||
clicon_err(OE_UNIX, errno, "unlink %s: %s", filename, strerror(errno));
|
||||
goto done;
|
||||
}
|
||||
retval = 0;
|
||||
|
|
@ -594,3 +602,64 @@ xmldb_print(clicon_handle h,
|
|||
done:
|
||||
return retval;
|
||||
}
|
||||
|
||||
/*! Rename an XML database
|
||||
* @param[in] h Clicon handle
|
||||
* @param[in] db Database name
|
||||
* @param[in] newdb New Database name; if NULL, then same as new
|
||||
* @param[in] suffix Suffix to append to new database name
|
||||
* @retval -1 Error
|
||||
* @retval 0 OK
|
||||
* @note if newdb and suffix are null, OK is returned as it is a no-op
|
||||
*/
|
||||
int
|
||||
xmldb_rename(clicon_handle h,
|
||||
const char *db,
|
||||
const char *newdb,
|
||||
const char *suffix)
|
||||
{
|
||||
char *old;
|
||||
char *fname;
|
||||
int retval = -1;
|
||||
|
||||
if ((xmldb_db2file(h, db, &old)) < 0) {
|
||||
goto done;
|
||||
};
|
||||
|
||||
if (newdb == NULL && suffix == NULL)
|
||||
// no-op
|
||||
goto done;
|
||||
|
||||
newdb = newdb == NULL ? old : newdb;
|
||||
suffix = suffix == NULL ? "" : suffix;
|
||||
|
||||
size_t size = strlen(newdb) + strlen(suffix);
|
||||
|
||||
if ((fname = malloc(size + 1)) == NULL) {
|
||||
clicon_err(OE_UNIX, errno, "malloc: %s", strerror(errno));
|
||||
goto done;
|
||||
};
|
||||
|
||||
int actual = 0;
|
||||
if ((actual = snprintf(fname, size, "%s%s", newdb, suffix)) < size) {
|
||||
clicon_err(OE_UNIX, 0, "snprintf wrote fewer bytes (%d) than requested (%zu)", actual, size);
|
||||
goto done;
|
||||
};
|
||||
|
||||
if ((rename(old, fname)) < 0) {
|
||||
clicon_err(OE_UNIX, errno, "rename: %s", strerror(errno));
|
||||
goto done;
|
||||
};
|
||||
|
||||
|
||||
retval = 0;
|
||||
|
||||
done:
|
||||
if (old)
|
||||
free(old);
|
||||
|
||||
if (fname)
|
||||
free(fname);
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,10 @@
|
|||
#include "clixon_netconf_lib.h"
|
||||
#include "clixon_proto_client.h"
|
||||
|
||||
#define PERSIST_ID_XML_FMT "<persist-id>%s</persist-id>"
|
||||
#define PERSIST_XML_FMT "<persist>%s</persist>"
|
||||
#define TIMEOUT_XML_FMT "<confirm-timeout>%u</confirm-timeout>"
|
||||
|
||||
/*! Connect to internal netconf socket
|
||||
*/
|
||||
int
|
||||
|
|
@ -1266,7 +1270,12 @@ clicon_rpc_validate(clicon_handle h,
|
|||
* @retval -1 Error and logged to syslog
|
||||
*/
|
||||
int
|
||||
clicon_rpc_commit(clicon_handle h)
|
||||
clicon_rpc_commit(clicon_handle h,
|
||||
int confirmed,
|
||||
int cancel,
|
||||
uint32_t timeout,
|
||||
char *persist,
|
||||
char *persist_id)
|
||||
{
|
||||
int retval = -1;
|
||||
struct clicon_msg *msg = NULL;
|
||||
|
|
@ -1274,15 +1283,65 @@ clicon_rpc_commit(clicon_handle h)
|
|||
cxobj *xerr;
|
||||
char *username;
|
||||
uint32_t session_id;
|
||||
char *persist_id_xml = NULL;
|
||||
char *persist_xml = NULL;
|
||||
char *timeout_xml = NULL;
|
||||
|
||||
if (persist_id) {
|
||||
if ((persist_id_xml = malloc(strlen(persist_id) + strlen(PERSIST_ID_XML_FMT) + 1)) == NULL) {
|
||||
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
|
||||
}
|
||||
sprintf(persist_id_xml, PERSIST_ID_XML_FMT, persist_id);
|
||||
}
|
||||
|
||||
if (persist) {
|
||||
if ((persist_xml = malloc(strlen(persist) + strlen(PERSIST_XML_FMT) + 1)) == NULL) {
|
||||
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
|
||||
};
|
||||
sprintf(persist_xml, PERSIST_XML_FMT, persist);
|
||||
}
|
||||
|
||||
if (timeout > 0) {
|
||||
|
||||
/* timeout is a uint32_t, so max value is 2^32, a 10-digit number, we'll just always malloc for a string that
|
||||
* may be that large rather than calculate the string length
|
||||
*/
|
||||
|
||||
if ((timeout_xml = malloc(10 + 1 + strlen(TIMEOUT_XML_FMT))) == NULL) {
|
||||
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
|
||||
};
|
||||
sprintf(timeout_xml, TIMEOUT_XML_FMT, timeout);
|
||||
}
|
||||
|
||||
|
||||
if (session_id_check(h, &session_id) < 0)
|
||||
goto done;
|
||||
username = clicon_username_get(h);
|
||||
if ((msg = clicon_msg_encode(session_id,
|
||||
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit/></rpc>",
|
||||
if (cancel) {
|
||||
msg = clicon_msg_encode(session_id,
|
||||
"<rpc xmlns=\"%s\" username=\"%s\" %s><cancel-commit>%s</cancel-commit></rpc>",
|
||||
NETCONF_BASE_NAMESPACE,
|
||||
username?username:"",
|
||||
NETCONF_MESSAGE_ID_ATTR)) == NULL)
|
||||
username ? username : "",
|
||||
NETCONF_MESSAGE_ID_ATTR,
|
||||
persist_id ? persist_id_xml : "");
|
||||
} else if (confirmed) {
|
||||
msg = clicon_msg_encode(session_id,
|
||||
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit><confirmed/>%s%s%s</commit></rpc>",
|
||||
NETCONF_BASE_NAMESPACE,
|
||||
username ? username : "",
|
||||
NETCONF_MESSAGE_ID_ATTR,
|
||||
timeout ? timeout_xml : "",
|
||||
persist_id ? persist_id_xml : "",
|
||||
persist ? persist_xml : "");
|
||||
} else {
|
||||
msg = clicon_msg_encode(session_id,
|
||||
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit>%s</commit></rpc>",
|
||||
NETCONF_BASE_NAMESPACE,
|
||||
username ? username : "",
|
||||
NETCONF_MESSAGE_ID_ATTR,
|
||||
persist ? persist_xml : "");
|
||||
}
|
||||
if (msg == NULL)
|
||||
goto done;
|
||||
if (clicon_rpc_msg(h, msg, &xret) < 0)
|
||||
goto done;
|
||||
|
|
@ -1296,6 +1355,12 @@ clicon_rpc_commit(clicon_handle h)
|
|||
xml_free(xret);
|
||||
if (msg)
|
||||
free(msg);
|
||||
if (persist_id_xml)
|
||||
free(persist_id_xml);
|
||||
if (persist_xml)
|
||||
free(persist_xml);
|
||||
if (timeout_xml)
|
||||
free(timeout_xml);
|
||||
return retval;
|
||||
}
|
||||
|
||||
|
|
@ -1479,7 +1544,7 @@ clicon_rpc_restconf_debug(clicon_handle h,
|
|||
clicon_err(OE_XML, 0, "rpc error"); /* XXX extract info from rpc-error */
|
||||
goto done;
|
||||
}
|
||||
if ((retval = clicon_rpc_commit(h)) < 0)
|
||||
if ((retval = clicon_rpc_commit(h, 0, 0, 0, NULL, NULL)) < 0)
|
||||
goto done;
|
||||
done:
|
||||
if (msg)
|
||||
|
|
|
|||
355
test/test_confirmed_commit.sh
Executable file
355
test/test_confirmed_commit.sh
Executable file
|
|
@ -0,0 +1,355 @@
|
|||
#!/usr/bin/env bash
|
||||
# Basic Netconf functionality
|
||||
# Mainly default/null prefix, but also xx: prefix
|
||||
# XXX: could add tests for dual prefixes xx and xy with doppelganger names, ie xy:filter that is
|
||||
# syntactic correct but wrong
|
||||
|
||||
# Magic line must be first in script (see README.md)
|
||||
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
|
||||
|
||||
APPNAME=example
|
||||
|
||||
cfg=$dir/conf_yang.xml
|
||||
tmp=$dir/tmp.x
|
||||
fyang=$dir/clixon-example.yang
|
||||
|
||||
# Use yang in example
|
||||
|
||||
cat <<EOF > $cfg
|
||||
<clixon-config xmlns="http://clicon.org/config">
|
||||
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
|
||||
<CLICON_FEATURE>ietf-netconf:startup</CLICON_FEATURE>
|
||||
<CLICON_FEATURE>ietf-netconf:confirmed-commit</CLICON_FEATURE>
|
||||
<CLICON_MODULE_SET_ID>42</CLICON_MODULE_SET_ID>
|
||||
<CLICON_YANG_DIR>${YANG_INSTALLDIR}</CLICON_YANG_DIR>
|
||||
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
|
||||
<CLICON_YANG_MAIN_FILE>$fyang</CLICON_YANG_MAIN_FILE>
|
||||
<CLICON_CLISPEC_DIR>/usr/local/lib/$APPNAME/clispec</CLICON_CLISPEC_DIR>
|
||||
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
|
||||
<CLICON_BACKEND_REGEXP>example_backend.so$</CLICON_BACKEND_REGEXP>
|
||||
<CLICON_NETCONF_DIR>/usr/local/lib/$APPNAME/netconf</CLICON_NETCONF_DIR>
|
||||
<CLICON_NETCONF_MESSAGE_ID_OPTIONAL>false</CLICON_NETCONF_MESSAGE_ID_OPTIONAL>
|
||||
<CLICON_RESTCONF_DIR>/usr/local/lib/$APPNAME/restconf</CLICON_RESTCONF_DIR>
|
||||
<CLICON_CLI_DIR>/usr/local/lib/$APPNAME/cli</CLICON_CLI_DIR>
|
||||
<CLICON_CLI_MODE>$APPNAME</CLICON_CLI_MODE>
|
||||
<CLICON_SOCK>$dir/$APPNAME.sock</CLICON_SOCK>
|
||||
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
|
||||
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
|
||||
</clixon-config>
|
||||
EOF
|
||||
|
||||
cat <<EOF > $fyang
|
||||
module clixon-example{
|
||||
yang-version 1.1;
|
||||
namespace "urn:example:clixon";
|
||||
prefix ex;
|
||||
import ietf-interfaces {
|
||||
prefix if;
|
||||
}
|
||||
import ietf-ip {
|
||||
prefix ip;
|
||||
}
|
||||
/* Example interface type for tests, local callbacks, etc */
|
||||
identity eth {
|
||||
base if:interface-type;
|
||||
}
|
||||
/* Generic config data */
|
||||
container table{
|
||||
list parameter{
|
||||
key name;
|
||||
leaf name{
|
||||
type string;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* State data (not config) for the example application*/
|
||||
container state {
|
||||
config false;
|
||||
description "state data for the example application (must be here for example get operation)";
|
||||
leaf-list op {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
augment "/if:interfaces/if:interface" {
|
||||
container my-status {
|
||||
config false;
|
||||
description "For testing augment+state";
|
||||
leaf int {
|
||||
type int32;
|
||||
}
|
||||
leaf str {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
}
|
||||
rpc client-rpc {
|
||||
description "Example local client-side RPC that is processed by the
|
||||
the netconf/restconf and not sent to the backend.
|
||||
This is a clixon implementation detail: some rpc:s
|
||||
are better processed by the client for API or perf reasons";
|
||||
input {
|
||||
leaf x {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
output {
|
||||
leaf x {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
}
|
||||
rpc empty {
|
||||
description "Smallest possible RPC with no input or output sections";
|
||||
}
|
||||
rpc example {
|
||||
description "Some example input/output for testing RFC7950 7.14.
|
||||
RPC simply echoes the input for debugging.";
|
||||
input {
|
||||
leaf x {
|
||||
description
|
||||
"If a leaf in the input tree has a 'mandatory' statement with
|
||||
the value 'true', the leaf MUST be present in an RPC invocation.";
|
||||
type string;
|
||||
mandatory true;
|
||||
}
|
||||
leaf y {
|
||||
description
|
||||
"If a leaf in the input tree has a 'mandatory' statement with the
|
||||
value 'true', the leaf MUST be present in an RPC invocation.";
|
||||
type string;
|
||||
default "42";
|
||||
}
|
||||
}
|
||||
output {
|
||||
leaf x {
|
||||
type string;
|
||||
}
|
||||
leaf y {
|
||||
type string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
EOF
|
||||
|
||||
function data() {
|
||||
if [[ "$1" == "" ]]
|
||||
then
|
||||
echo "<data/>"
|
||||
else
|
||||
echo "<data>$1</data>"
|
||||
fi
|
||||
}
|
||||
|
||||
# Pipe stdin to command and also do chunked framing (netconf 1.1)
|
||||
# Arguments:
|
||||
# - Command
|
||||
# - expected command return value (0 if OK)
|
||||
# - stdin input1 This is NOT encoded, eg preamble/hello
|
||||
# - stdin input2 This gets chunked encoding
|
||||
# - expect1 stdout outcome, can be partial and contain regexps
|
||||
# - expect2 stdout outcome This gets chunked encoding, must be complete netconf message
|
||||
# Use this if you want regex eg ^foo$
|
||||
|
||||
function rpc() {
|
||||
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS>$1</rpc>" "" "<rpc-reply $DEFAULTNS>$2</rpc-reply>"
|
||||
}
|
||||
|
||||
function commit() {
|
||||
if [[ "$1" == "" ]]
|
||||
then
|
||||
rpc "<commit/>" "<ok/>"
|
||||
else
|
||||
rpc "<commit>$1</commit>" "<ok/>"
|
||||
fi
|
||||
}
|
||||
|
||||
function edit_config() {
|
||||
TARGET="$1"
|
||||
CONFIG="$2"
|
||||
rpc "<edit-config><target><$TARGET/></target><config>$CONFIG</config></edit-config>" "<ok/>"
|
||||
}
|
||||
|
||||
function assert_config_equals() {
|
||||
TARGET="$1"
|
||||
EXPECTED="$2"
|
||||
rpc "<get-config><source><$TARGET/></source></get-config>" "$(data "$EXPECTED")"
|
||||
}
|
||||
|
||||
function reset() {
|
||||
rpc "<edit-config><target><candidate/></target><default-operation>none</default-operation><config operation=\"delete\"/></edit-config>" "<ok/>"
|
||||
commit
|
||||
assert_config_equals "candidate" ""
|
||||
assert_config_equals "running" ""
|
||||
}
|
||||
|
||||
CANDIDATE_PATH="/usr/local/var/$APPNAME/candidate_db"
|
||||
RUNNING_PATH="/usr/local/var/$APPNAME/running_db"
|
||||
ROLLBACK_PATH="/usr/local/var/$APPNAME/rollback_db"
|
||||
FAILSAFE_PATH="/usr/local/var/$APPNAME/failsafe_db"
|
||||
|
||||
CONFIGB="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth0</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
|
||||
CONFIGC="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth1</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
|
||||
CONFIGBPLUSC="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth0</name><type>ex:eth</type><enabled>true</enabled></interface><interface><name>eth1</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
|
||||
FAILSAFE_CFG="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth99</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
|
||||
|
||||
# TODO this test suite is somewhat brittle as it relies on the presence of the example configuration that one gets with
|
||||
# make install-example in the Clixon distribution. It would be better if the dependencies were entirely self contained.
|
||||
|
||||
new "test params: -f $cfg -- -s"
|
||||
# Bring your own backend
|
||||
if [ $BE -ne 0 ]; then
|
||||
# kill old backend (if any)
|
||||
new "kill old backend"
|
||||
sudo clixon_backend -zf $cfg
|
||||
if [ $? -ne 0 ]; then
|
||||
err
|
||||
fi
|
||||
new "start backend -s init -f $cfg -- -s"
|
||||
start_backend -s init -f $cfg -- -s
|
||||
fi
|
||||
|
||||
new "wait backend"
|
||||
wait_backend
|
||||
|
||||
new "netconf ephemeral confirmed-commit rolls back after disconnect"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
assert_config_equals "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><confirm-timeout>30</confirm-timeout>"
|
||||
assert_config_equals "running" ""
|
||||
|
||||
new "netconf persistent confirmed-commit"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><persist>a</persist>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
edit_config "candidate" "$CONFIGC"
|
||||
commit "<confirmed/><persist>ab</persist><persist-id>a</persist-id>"
|
||||
assert_config_equals "running" "$CONFIGBPLUSC"
|
||||
|
||||
new "netconf cancel-commit with invalid persist-id"
|
||||
rpc "<cancel-commit><persist-id>abc</persist-id></cancel-commit>" "<rpc-error><error-type>application</error-type><error-tag>invalid-value</error-tag><error-severity>error</error-severity><error-message>a confirmed-commit with the given persist-id was not found</error-message></rpc-error>"
|
||||
|
||||
new "netconf cancel-commit with valid persist-id"
|
||||
rpc "<cancel-commit><persist-id>ab</persist-id></cancel-commit>" "<ok/>"
|
||||
|
||||
new "netconf persistent confirmed-commit with timeout"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><confirm-timeout>2</confirm-timeout><persist>abcd</persist>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
sleep 2
|
||||
assert_config_equals "running" ""
|
||||
|
||||
new "netconf persistent confirmed-commit with reset timeout"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><persist>abcde</persist><confirm-timeout>5</confirm-timeout>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
edit_config "candidate" "$CONFIGC"
|
||||
commit "<confirmed/><persist-id>abcde</persist-id><persist>abcdef</persist><confirm-timeout>10</confirm-timeout>"
|
||||
# prove the new timeout is active by sleeping longer than first timeout. get config, assert == B+C
|
||||
sleep 6
|
||||
assert_config_equals "running" "$CONFIGBPLUSC"
|
||||
# now sleep long enough for rollback to happen; get config, assert == A
|
||||
sleep 5
|
||||
assert_config_equals "running" ""
|
||||
|
||||
new "netconf persistent confirming-commit to epehemeral confirmed-commit should rollback"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><persist/><confirm-timeout>10</confirm-timeout>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
commit "<confirmed/><persist-id/>"
|
||||
assert_config_equals "running" ""
|
||||
|
||||
new "netconf confirming-commit for persistent confirmed-commit with empty persist value"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit "<confirmed/><persist/><confirm-timeout>10</confirm-timeout>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
commit "<persist-id/>"
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
|
||||
# TODO the next two tests are broken. The whole idea of presence or absence of rollback_db indicating something might
|
||||
# need reconsideration. see clixon_datastore.c#xmldb_delete() and backend_startup.c#startup_mode_startup()
|
||||
|
||||
new "backend loads rollback if present at startup"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
commit ""
|
||||
edit_config "candidate" "$CONFIGC"
|
||||
commit "<persist>abcdefg</persist><confirmed/>"
|
||||
assert_config_equals "running" "$CONFIGBPLUSC"
|
||||
stop_backend -f $cfg # kill backend and restart
|
||||
[ -f "$ROLLBACK_PATH" ] || err "rollback_db doesn't exist!" # assert rollback_db exists
|
||||
start_backend -s running -f $cfg -- -s
|
||||
wait_backend
|
||||
assert_config_equals "running" "$CONFIGB"
|
||||
[ -f "ROLLBACK_PATH" ] && err "rollback_db still exists!" # assert rollback_db doesn't exist
|
||||
|
||||
stop_backend -f $cfg
|
||||
start_backend -s init -f $cfg -- -s
|
||||
|
||||
new "backend loads failsafe at startup if rollback present but cannot be loaded"
|
||||
reset
|
||||
|
||||
sudo tee "$FAILSAFE_PATH" > /dev/null << EOF # create a failsafe database
|
||||
<config>$FAILSAFE_CFG</config>
|
||||
EOF
|
||||
|
||||
edit_config "candidate" "$CONFIGC"
|
||||
commit "<persist>foobar</persist><confirmed/>"
|
||||
assert_config_equals "running" "$CONFIGC"
|
||||
stop_backend -f $cfg # kill the backend
|
||||
sudo rm $ROLLBACK_PATH # modify rollback_db so it won't commit successfully
|
||||
sudo tee "$ROLLBACK_PATH" > /dev/null << EOF
|
||||
<foo>
|
||||
<bar>
|
||||
<baz/>
|
||||
</bar>
|
||||
</foo>
|
||||
EOF
|
||||
start_backend -s running -f $cfg -- -s
|
||||
wait_backend
|
||||
assert_config_equals "running" "$FAILSAFE_CFG"
|
||||
|
||||
|
||||
# TODO this test is now broken too, but not sure why; suspicion that the initial confirmed-commit session is not kept alive as intended
|
||||
stop_backend -f $cfg
|
||||
start_backend -s init -f $cfg -lf/tmp/clixon.log -D1 -- -s
|
||||
wait_backend
|
||||
new "ephemeral confirmed-commit survives unrelated ephemeral session disconnect"
|
||||
reset
|
||||
edit_config "candidate" "$CONFIGB"
|
||||
# start a new ephemeral confirmed commit, but keep the confirmed-commit session alive (need to put it in the background)
|
||||
sleep 60 | cat <(echo "$DEFAULTHELLO<rpc $DEFAULTNS><commit><confirmed/><confirm-timeout>60</confirm-timeout></commit></rpc>]]>]]>") -| $clixon_netconf -qf $cfg >> /dev/null &
|
||||
PIDS=($(jobs -l % | cut -c 6- | awk '{print $1}'))
|
||||
assert_config_equals "running" "$CONFIGB" # assert config twice to prove it surives disconnect
|
||||
assert_config_equals "running" "$CONFIGB" # of ephemeral sessions
|
||||
|
||||
kill -9 ${PIDS[0]} # kill the while loop above to close STDIN on 1st
|
||||
# ephemeral session and cause rollback
|
||||
assert_config_equals "running" ""
|
||||
|
||||
|
||||
# TODO test same cli methods as tested for netconf
|
||||
# TODO test restconf receives "409 conflict" when there is a persistent confirmed-commit active
|
||||
# TODO test restconf causes confirming-commit for ephemeral confirmed-commit
|
||||
|
||||
|
||||
if [ $BE -ne 0 ]; then
|
||||
new "Kill backend"
|
||||
# Check if premature kill
|
||||
pid=$(pgrep -u root -f clixon_backend)
|
||||
if [ -z "$pid" ]; then
|
||||
err "backend already dead"
|
||||
fi
|
||||
# kill backend
|
||||
stop_backend -f $cfg
|
||||
fi
|
||||
|
||||
new "endtest"
|
||||
endtest
|
||||
Loading…
Add table
Add a link
Reference in a new issue