diff --git a/apps/backend/Makefile.in b/apps/backend/Makefile.in
index 9bfa8d92..5e3a782a 100644
--- a/apps/backend/Makefile.in
+++ b/apps/backend/Makefile.in
@@ -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
diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c
index 7164bca7..94866a9b 100644
--- a/apps/backend/backend_client.c
+++ b/apps/backend/backend_client.c
@@ -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,7 +455,20 @@ from_client_edit_config(clicon_handle h,
autocommit = 1;
/* If autocommit option is set or requested by client */
if (clicon_autocommit(h) || autocommit) {
- if ((ret = candidate_commit(h, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
+ // 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;
xmldb_copy(h, "running", "candidate");
diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c
index d6326d3c..658e5f72 100644
--- a/apps/backend/backend_commit.c
+++ b/apps/backend/backend_commit.c
@@ -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 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 ..., ; the confirming-commit MUST now be accompanied by a matching
+ *
+ */
+ 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 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;
@@ -719,7 +984,178 @@ candidate_commit(clicon_handle h,
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 must either:
+ * 1) be presented without a value, and on the same session as a prior confirmed-commit that itself was
+ * without a value, OR
+ * 2) be presented with a value that matches the 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 ..., matching the prior confirming-commit's */
+ 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 , the prior confirming-commit lacked , 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 RPC constitutes a "confirming-commit", roughly meaning:
+ * 1) it was issued in the same session as a prior confirmed-commit
+ * 2) it bears a element matching the element that accompanied the prior confirmed-commit
+ *
+ * If it is a valid "confirming-commit" and this RPC does not bear another 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 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 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, "", 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:
@@ -850,10 +1335,86 @@ from_client_cancel_commit(clicon_handle h,
void *arg,
void *regarg)
{
- int retval = -1;
- retval = 0;
- // done:
- return retval;
+ 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, "", 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.
diff --git a/apps/backend/backend_failsafe.c b/apps/backend/backend_failsafe.c
new file mode 100644
index 00000000..4c6f0966
--- /dev/null
+++ b/apps/backend/backend_failsafe.c
@@ -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
+#include
+#include
+
+/* cligen */
+#include
+
+/* clixon */
+#include
+
+#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;
+}
diff --git a/apps/backend/backend_failsafe.h b/apps/backend/backend_failsafe.h
new file mode 100644
index 00000000..55e0dbf3
--- /dev/null
+++ b/apps/backend/backend_failsafe.h
@@ -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
diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c
index ac8bc69d..727a73c8 100644
--- a/apps/backend/backend_main.c
+++ b/apps/backend/backend_main.c
@@ -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;
diff --git a/apps/backend/backend_startup.c b/apps/backend/backend_startup.c
index 0b065ae4..7f7a02c8 100644
--- a/apps/backend/backend_startup.c
+++ b/apps/backend/backend_startup.c
@@ -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;
diff --git a/apps/backend/clixon_backend_commit.h b/apps/backend/clixon_backend_commit.h
index 8f630878..2cd1c1a6 100644
--- a/apps/backend/clixon_backend_commit.h
+++ b/apps/backend/clixon_backend_commit.h
@@ -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 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);
diff --git a/apps/cli/cli_common.c b/apps/cli/cli_common.c
index 1fd41604..158921b6 100644
--- a/apps/cli/cli_common.c
+++ b/apps/cli/cli_common.c
@@ -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;
+
+ 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)) < 0)
+ if ((retval = clicon_rpc_commit(h, confirmed, cancel, timeout, persist, persist_id)) < 0)
goto done;
retval = 0;
done:
diff --git a/example/main/example.xml.in b/example/main/example.xml.in
index e03d82f1..4cb2a3e4 100644
--- a/example/main/example.xml.in
+++ b/example/main/example.xml.in
@@ -1,6 +1,7 @@
/usr/local/etc/example.xmlietf-netconf:startup
+ ietf-netconf:confirmed-commitclixon-restconf:allow-auth-noneclixon-restconf:fcgi/usr/local/share/clixon
diff --git a/example/main/example_cli.cli b/example/main/example_cli.cli
index e06c2c92..0da1a6a1 100644
--- a/example/main/example_cli.cli
+++ b/example/main/example_cli.cli
@@ -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") ("The 'persist' value of the persistent confirmed-commit")], cli_commit(); {
+ ("Cancel an ongoing confirmed-commit"), cli_commit();
+ ("Require a confirming commit") {
+ [persist("Make this confirmed-commit persistent") ("The value that must be provided as 'persist-id' in the confirming-commit or cancel-commit")]
+ [("The rollback timeout in seconds")], cli_commit();
+ }
+ }
+}
quit("Quit"), cli_quit();
debug("Debugging parts of the system"){
diff --git a/lib/clixon/clixon_datastore.h b/lib/clixon/clixon_datastore.h
index 680dfc12..954fb700 100644
--- a/lib/clixon/clixon_datastore.h
+++ b/lib/clixon/clixon_datastore.h
@@ -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 */
diff --git a/lib/clixon/clixon_proto_client.h b/lib/clixon/clixon_proto_client.h
index 76ff0fb9..644c8d81 100644
--- a/lib/clixon/clixon_proto_client.h
+++ b/lib/clixon/clixon_proto_client.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);
diff --git a/lib/src/clixon_datastore.c b/lib/src/clixon_datastore.c
index bad333b0..8048f4b3 100644
--- a/lib/src/clixon_datastore.c
+++ b/lib/src/clixon_datastore.c
@@ -418,10 +418,18 @@ 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);
- goto done;
- }
+ // 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;
done:
if (filename)
@@ -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;
+}
diff --git a/lib/src/clixon_proto_client.c b/lib/src/clixon_proto_client.c
index 06afc117..ee724a44 100644
--- a/lib/src/clixon_proto_client.c
+++ b/lib/src/clixon_proto_client.c
@@ -83,6 +83,10 @@
#include "clixon_netconf_lib.h"
#include "clixon_proto_client.h"
+#define PERSIST_ID_XML_FMT "%s"
+#define PERSIST_XML_FMT "%s"
+#define TIMEOUT_XML_FMT "%u"
+
/*! 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,
- "",
- NETCONF_BASE_NAMESPACE,
- username?username:"",
- NETCONF_MESSAGE_ID_ATTR)) == NULL)
+ if (cancel) {
+ msg = clicon_msg_encode(session_id,
+ "%s",
+ NETCONF_BASE_NAMESPACE,
+ username ? username : "",
+ NETCONF_MESSAGE_ID_ATTR,
+ persist_id ? persist_id_xml : "");
+ } else if (confirmed) {
+ msg = clicon_msg_encode(session_id,
+ "%s%s%s",
+ 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,
+ "%s",
+ 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)
diff --git a/test/test_confirmed_commit.sh b/test/test_confirmed_commit.sh
new file mode 100755
index 00000000..b5ee69f1
--- /dev/null
+++ b/test/test_confirmed_commit.sh
@@ -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 < $cfg
+
+ $cfg
+ ietf-netconf:startup
+ ietf-netconf:confirmed-commit
+ 42
+ ${YANG_INSTALLDIR}
+ $IETFRFC
+ $fyang
+ /usr/local/lib/$APPNAME/clispec
+ /usr/local/lib/$APPNAME/backend
+ example_backend.so$
+ /usr/local/lib/$APPNAME/netconf
+ false
+ /usr/local/lib/$APPNAME/restconf
+ /usr/local/lib/$APPNAME/cli
+ $APPNAME
+ $dir/$APPNAME.sock
+ /usr/local/var/$APPNAME/$APPNAME.pidfile
+ /usr/local/var/$APPNAME
+
+EOF
+
+cat < $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 ""
+ else
+ echo "$1"
+ 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" "$1" "" "$2"
+}
+
+function commit() {
+ if [[ "$1" == "" ]]
+ then
+ rpc "" ""
+ else
+ rpc "$1" ""
+ fi
+}
+
+function edit_config() {
+ TARGET="$1"
+ CONFIG="$2"
+ rpc "<$TARGET/>$CONFIG" ""
+}
+
+function assert_config_equals() {
+ TARGET="$1"
+ EXPECTED="$2"
+ rpc "<$TARGET/>" "$(data "$EXPECTED")"
+}
+
+function reset() {
+ rpc "none" ""
+ 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="eth0ex:ethtrue"
+CONFIGC="eth1ex:ethtrue"
+CONFIGBPLUSC="eth0ex:ethtrueeth1ex:ethtrue"
+FAILSAFE_CFG="eth99ex:ethtrue"
+
+# 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 "30"
+assert_config_equals "running" ""
+
+new "netconf persistent confirmed-commit"
+reset
+edit_config "candidate" "$CONFIGB"
+commit "a"
+assert_config_equals "running" "$CONFIGB"
+edit_config "candidate" "$CONFIGC"
+commit "aba"
+assert_config_equals "running" "$CONFIGBPLUSC"
+
+new "netconf cancel-commit with invalid persist-id"
+rpc "abc" "applicationinvalid-valueerrora confirmed-commit with the given persist-id was not found"
+
+new "netconf cancel-commit with valid persist-id"
+rpc "ab" ""
+
+new "netconf persistent confirmed-commit with timeout"
+reset
+edit_config "candidate" "$CONFIGB"
+commit "2abcd"
+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 "abcde5"
+assert_config_equals "running" "$CONFIGB"
+edit_config "candidate" "$CONFIGC"
+commit "abcdeabcdef10"
+# 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 "10"
+assert_config_equals "running" "$CONFIGB"
+commit ""
+assert_config_equals "running" ""
+
+new "netconf confirming-commit for persistent confirmed-commit with empty persist value"
+reset
+edit_config "candidate" "$CONFIGB"
+commit "10"
+assert_config_equals "running" "$CONFIGB"
+commit ""
+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 "abcdefg"
+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
+$FAILSAFE_CFG
+EOF
+
+edit_config "candidate" "$CONFIGC"
+commit "foobar"
+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
+
+
+
+
+
+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 "$DEFAULTHELLO60]]>]]>") -| $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