diff --git a/apps/backend/Makefile.in b/apps/backend/Makefile.in
index 9bfa8d92..c5c2f409 100644
--- a/apps/backend/Makefile.in
+++ b/apps/backend/Makefile.in
@@ -100,6 +100,7 @@ APPOBJ = $(APPSRC:.c=.o)
LIBSRC = clixon_backend_transaction.c
LIBSRC += clixon_backend_handle.c
LIBSRC += backend_commit.c
+LIBSRC += backend_confirm.c
LIBSRC += backend_plugin.c
LIBOBJ = $(LIBSRC:.c=.o)
diff --git a/apps/backend/backend_client.c b/apps/backend/backend_client.c
index 95cfdda3..4edc7915 100644
--- a/apps/backend/backend_client.c
+++ b/apps/backend/backend_client.c
@@ -181,18 +181,18 @@ backend_client_rm(clicon_handle h,
}
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
- if (confirmed_commit.state == EPHEMERAL) {
+ if (confirmed_commit_state_get(h) == 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);
+ clicon_debug(1, "session_id: %u, confirmed_commit.session_id: %u", ce->ce_id, confirmed_commit_session_id_get(h));
- if (myid == confirmed_commit.session_id) {
+ if (myid == confirmed_commit_session_id_get(h)) {
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();
+ cancel_rollback_event(h);
do_rollback(h, NULL);
}
}
@@ -469,7 +469,7 @@ from_client_edit_config(clicon_handle h,
* status-line. The error-tag "in-use" is used in this case.
*/
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
- switch (confirmed_commit.state){
+ switch (confirmed_commit_state_get(h)){
case INACTIVE:
break;
case PERSISTENT:
@@ -483,7 +483,7 @@ from_client_edit_config(clicon_handle h,
break;
}
}
- if ((ret = candidate_commit(h, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
+ if ((ret = candidate_commit(h, NULL, "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_client.h b/apps/backend/backend_client.h
index 8aaf2244..18251e69 100644
--- a/apps/backend/backend_client.h
+++ b/apps/backend/backend_client.h
@@ -51,7 +51,7 @@ struct client_entry{
int ce_nr; /* Client number (for dbg/tracing) */
int ce_stat_in; /* Nr of received msgs from client */
int ce_stat_out;/* Nr of sent msgs to client */
- int ce_id; /* Session id */
+ uint32_t ce_id; /* Session id, accessor functions: clicon_session_id_get/set */
char *ce_username;/* Translated from peer user cred */
clicon_handle ce_handle; /* clicon config handle (all clients have same?) */
};
diff --git a/apps/backend/backend_commit.c b/apps/backend/backend_commit.c
index 58af5c3c..f8337968 100644
--- a/apps/backend/backend_commit.c
+++ b/apps/backend/backend_commit.c
@@ -32,7 +32,7 @@
the terms of any one of the Apache License version 2 or the GPL.
***** END LICENSE BLOCK *****
-
+ Commit and validate
*/
#ifdef HAVE_CONFIG_H
@@ -71,14 +71,6 @@
#include "clixon_backend_commit.h"
#include "backend_client.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
*
* Key values are checked as follows:
@@ -642,275 +634,24 @@ 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(void)
-{
- 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_CRIT, "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;
-}
-
-/*! Cancel a confirming commit by removing rollback, and free state
- * @param[in] h
- * @param[out] cbret
- * @retval 0 OK
- */
-int
-cancel_confirmed_commit(clicon_handle h)
-{
- 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");
- return 0;
-}
-
-/*! 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
* fails, we just ignore the errors and proceed. Maybe we should
* do something more drastic?
- * @param[in] h Clicon handle
- * @param[in] db A candidate database, not necessarily "candidate"
- * @retval -1 Error - or validation failed
- * @retval 0 Validation failed (with cbret set)
- * @retval 1 Validation OK
+ * @param[in] h Clicon handle
+ * @param[in] xe Request: (or NULL)
+ * @param[in] session_id Client session id, only if xe
+ * @param[in] db A candidate database, not necessarily "candidate"
+ * @param[out] cbret Return xml tree, eg ..., 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
@@ -1226,17 +797,10 @@ from_client_commit(clicon_handle h,
}
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_confirmed_commit(h);
- cprintf(cbret, "", NETCONF_BASE_NAMESPACE);
- goto ok;
- }
+ if ((ret = from_client_confirmed_commit(h, xe, myid, cbret)) < 0)
+ goto done;
+ if (ret == 0)
+ goto ok;
}
/* Check if target locked by other client */
@@ -1251,7 +815,7 @@ from_client_commit(clicon_handle h,
goto done;
goto ok;
}
- if ((ret = candidate_commit(h, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
+ if ((ret = candidate_commit(h, xe, "candidate", cbret)) < 0){ /* Assume validation fail, nofatal */
clicon_debug(1, "Commit candidate failed");
if (ret < 0)
if (netconf_operation_failed(cbret, "application", clicon_err_reason)< 0)
@@ -1263,7 +827,6 @@ 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 */
@@ -1321,111 +884,6 @@ from_client_discard_changes(clicon_handle h,
return retval; /* may be zero if we ignoring errors from commit */
}
-/*! Cancel an ongoing confirmed commit.
- * If the confirmed commit is persistent, the parameter 'persist-id' must be
- * given, and it must match the value of the 'persist' parameter.
- * If the confirmed-commit is ephemeral, the 'persist-id' must not be given and both the confirmed-commit and the
- * cancel-commit must originate from the same session.
- *
- * @param[in] h Clicon handle
- * @param[in] xe Request:
- * @param[out] cbret Return xml tree, eg ..., ", NETCONF_BASE_NAMESPACE);
- retval = 0;
- clicon_log(LOG_INFO, "a confirmed-commit has been cancelled by client request");
- }
- goto done;
-
- netconf_response_error:
- clicon_err(OE_DAEMON, 0, "failed to write netconf response");
-
- done:
- return retval;
-}
-
/*! Validates the contents of the specified configuration.
* @param[in] h Clicon handle
* @param[in] xe Request:
@@ -1624,7 +1082,7 @@ load_failsafe(clicon_handle h,
goto done;
if (xmldb_db_reset(h, "running") < 0)
goto done;
- ret = candidate_commit(h, db, cbret);
+ ret = candidate_commit(h, NULL, db, cbret);
if (ret != 1)
if (xmldb_copy(h, "tmp", "running") < 0)
goto done;
diff --git a/apps/backend/backend_confirm.c b/apps/backend/backend_confirm.c
new file mode 100644
index 00000000..b21b13bc
--- /dev/null
+++ b/apps/backend/backend_confirm.c
@@ -0,0 +1,834 @@
+/*
+ *
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
+ Copyright (C) 2017-2019 Olof Hagsand
+ Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate)
+
+ This file is part of CLIXON.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+ Alternatively, the contents of this file may be used under the terms of
+ the GNU General Public License Version 3 or later (the "GPL"),
+ in which case the provisions of the GPL are applicable instead
+ of those above. If you wish to allow use of your version of this file only
+ under the terms of the GPL, and not to allow others to
+ use your version of this file under the terms of Apache License version 2,
+ indicate your decision by deleting the provisions above and replace them with
+ the notice and other provisions required by the GPL. If you do not delete
+ the provisions above, a recipient may use your version of this file under
+ the terms of any one of the Apache License version 2 or the GPL.
+
+ ***** END LICENSE BLOCK *****
+ Commit-confirm
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "clixon_config.h" /* generated by config & autoconf */
+#endif
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* cligen */
+#include
+
+/* clicon */
+#include
+
+#include "clixon_backend_transaction.h"
+#include "clixon_backend_plugin.h"
+#include "backend_handle.h"
+#include "clixon_backend_commit.h"
+#include "backend_client.h"
+
+/*
+ * Local constants
+ */
+/*! Use a global variable to :
+ * if an RPC bearing satisfies conditions to cancel the rollback timer
+ */
+#undef _GLOBAL_VALID_CONFIRMING_COMMIT
+
+/*
+ * Local types
+ */
+/* 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 cc_state;
+ char *cc_persist_id; /* a value given by a client in the confirmed-commit */
+ uint32_t cc_session_id; /* the session_id of the client that gave no value */
+ int (*cc_fn)(int, void*); /* function pointer for rollback event (rollback_fn()) */
+ void *cc_arg; /* clicon_handle that will be passed to rollback_fn() */
+};
+
+#ifdef _GLOBAL_VALID_CONFIRMING_COMMIT
+/*
+ * Local global variables
+ */
+/* if an RPC bearing satisfies conditions to cancel the rollback timer */
+static int _is_valid_confirming_commit = 0;
+#endif
+
+int
+confirmed_commit_init(clicon_handle h)
+{
+ int retval = -1;
+ struct confirmed_commit *cc = NULL;
+
+ if ((cc = calloc(1, sizeof(*cc))) == NULL){
+ clicon_err(OE_UNIX, errno, "calloc");
+ goto done;
+ }
+ cc->cc_state = INACTIVE;
+ if (clicon_ptr_set(h, "confirmed-commit-struct", cc) < 0)
+ goto done;
+ retval = 0;
+ done:
+ return retval;
+}
+
+/*! If confirm commit persist-id exists, free it
+ * @param[in] h Clixon handle
+ * @retval 0 OK
+ */
+int
+confirmed_commit_free(clicon_handle h)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ if (cc != NULL){
+ if (cc->cc_persist_id != NULL)
+ free (cc->cc_persist_id);
+ free(cc);
+ }
+ clicon_ptr_del(h, "confirmed-commit-struct");
+ return 0;
+}
+
+/*
+ * Accessor functions
+ */
+enum confirmed_commit_state
+confirmed_commit_state_get(clicon_handle h)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ return cc->cc_state;
+}
+
+static int
+confirmed_commit_state_set(clicon_handle h,
+ enum confirmed_commit_state state)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ cc->cc_state = state;
+ return 0;
+}
+
+char *
+confirmed_commit_persist_id_get(clicon_handle h)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ return cc->cc_persist_id;
+}
+
+static int
+confirmed_commit_persist_id_set(clicon_handle h,
+ char *persist_id)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ if (cc->cc_persist_id)
+ free(cc->cc_persist_id);
+ if (persist_id){
+ if ((cc->cc_persist_id = strdup4(persist_id)) == NULL){
+ clicon_err(OE_UNIX, errno, "strdup4");
+ return -1;
+ }
+ }
+ else
+ cc->cc_persist_id = NULL;
+ return 0;
+}
+
+uint32_t
+confirmed_commit_session_id_get(clicon_handle h)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ return cc->cc_session_id;
+}
+
+static int
+confirmed_commit_session_id_set(clicon_handle h,
+ uint32_t session_id)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ cc->cc_session_id = session_id;
+ return 0;
+}
+
+static int
+confirmed_commit_fn_arg_get(clicon_handle h,
+ int (**fn)(int, void*),
+ void **arg)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ *fn = cc->cc_fn;
+ *arg = cc->cc_arg;
+ return 0;
+}
+
+static int
+confirmed_commit_fn_arg_set(clicon_handle h,
+ int (*fn)(int, void*),
+ void *arg)
+{
+ struct confirmed_commit *cc = NULL;
+
+ clicon_ptr_get(h, "confirmed-commit-struct", (void**)&cc);
+ cc->cc_fn = fn;
+ cc->cc_arg = arg;
+ return 0;
+}
+
+/*! Return if confirmed tag found
+ * @param[in] xe Commit rpc xml
+ * @retval 1 Confirmed tag exists
+ * @retval 0 Confirmed tag does not exist
+ */
+static int
+xe_confirmed(cxobj *xe)
+{
+ return (xml_find_type(xe, NULL, "confirmed", CX_ELMNT) != NULL) ? 1 : 0;
+}
+
+/*! Return if persist exists and its string value field
+ * @param[in] xe Commit rpc xml
+ * @param[out] str Pointer to persist
+ * @retval 1 Persist field exists
+ * @retval 0 Persist field does not exist
+ */
+static int
+xe_persist(cxobj *xe,
+ char **str)
+{
+ cxobj *xml;
+
+ if ((xml = xml_find_type(xe, NULL, "persist", CX_ELMNT)) != NULL){
+ *str = xml_body(xml);
+ return 1;
+ }
+ *str = NULL;
+ return 0;
+}
+
+/*! Return if persist-id exists and its string value
+ *
+ * @param[in] xe Commit rpc xml
+ * @param[out] str Pointer to persist-id
+ * @retval 1 Persist-id exists
+ * @retval 0 Persist-id does not exist
+ */
+static int
+xe_persist_id(cxobj *xe,
+ char **str)
+{
+ cxobj *xml;
+
+ if ((xml = xml_find_type(xe, NULL, "persist-id", CX_ELMNT)) != NULL){
+ *str = xml_body(xml);
+ return 1;
+ }
+ *str = NULL;
+ return 0;
+}
+
+/*! Return timeout
+ * @param[in] xe Commit rpc xml
+ * @retval sec Timeout in seconds, can be 0 if no timeout exists or is zero
+ */
+static unsigned int
+xe_timeout(cxobj *xe)
+{
+ cxobj *xml;
+ char *str;
+
+ if ((xml = xml_find_type(xe, NULL, "confirm-timeout", CX_ELMNT)) != NULL &&
+ (str = xml_body(xml)) != NULL)
+ return strtoul(str, NULL, 10);
+ return 0;
+}
+
+/*! Cancel a scheduled rollback as previously registered by schedule_rollback_event()
+ *
+ * @param[in] h Clixon handle
+ * @retval 0 Rollback event successfully cancelled
+ * @retval -1 No Rollback event was found
+ */
+int
+cancel_rollback_event(clicon_handle h)
+{
+ int retval;
+ int (*fn)(int, void*) = NULL;
+ void *arg = NULL;
+
+ confirmed_commit_fn_arg_get(h, &fn, &arg);
+ if ((retval = clixon_event_unreg_timeout(fn, 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_CRIT, "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_arg_set(h, rollback_fn, 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;
+}
+
+/*! Cancel a confirming commit by removing rollback, and free state
+ * @param[in] h
+ * @param[out] cbret
+ * @retval 0 OK
+ */
+int
+cancel_confirmed_commit(clicon_handle h)
+{
+ cancel_rollback_event(h);
+
+ if (confirmed_commit_state_get(h) == PERSISTENT &&
+ confirmed_commit_persist_id_get(h) != NULL) {
+ confirmed_commit_persist_id_set(h, NULL);
+ }
+
+ confirmed_commit_state_set(h, INACTIVE);
+
+ if (xmldb_delete(h, "rollback") < 0)
+ clicon_err(OE_DB, 0, "Error deleting the rollback configuration");
+ return 0;
+}
+
+/*! 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] xe Request:
+ * @param[in] myid current client session-id
+ * @retval 1 The confirming-commit is valid
+ * @retval 0 The confirming-commit is not valid
+ * @retval -1 Error
+ */
+static int
+check_valid_confirming_commit(clicon_handle h,
+ cxobj *xe,
+ uint32_t myid)
+{
+ int retval = -1;
+ char *persist_id = NULL;
+
+ if (xe == NULL){
+ clicon_err(OE_CFG, EINVAL, "xe is NULL");
+ goto done;
+ }
+ switch (confirmed_commit_state_get(h)) {
+ case PERSISTENT:
+ if (xe_persist_id(xe, &persist_id)) {
+ if (clicon_strcmp(persist_id, confirmed_commit_persist_id_get(h)) == 0) {
+ /* the RPC included a matching the prior confirming-commit's */
+ break; // valid
+ }
+ else {
+ clicon_log(LOG_INFO,
+ "a persistent confirmed-commit is in progress but the client issued a "
+ "confirming-commit with an incorrect persist-id");
+ goto invalid;
+ }
+ } else {
+ clicon_log(LOG_INFO,
+ "a persistent confirmed-commit is in progress but the client issued a confirming-commit"
+ "without a persist-id");
+ goto invalid;
+ }
+ case EPHEMERAL:
+ if (myid == confirmed_commit_session_id_get(h)) {
+ /* the RPC lacked a , the prior confirming-commit lacked , and both were issued
+ * on the same session.
+ */
+ break; // valid
+ }
+ 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");
+ goto invalid;
+ default:
+ clicon_debug(1, "commit-confirmed state !? %d", confirmed_commit_state_get(h));
+ goto invalid;
+ }
+ retval = 1; // valid
+ done:
+ return retval;
+ invalid:
+ retval = 0;
+ goto done;
+}
+
+/*! 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[in] xe Commit rpc xml or NULL
+ * @retval 0 OK
+ * @retval -1 Error
+ */
+int
+handle_confirmed_commit(clicon_handle h,
+ cxobj *xe)
+{
+ int retval = -1;
+ uint32_t session_id;
+ char *persist;
+ unsigned long confirm_timeout = 0L;
+ int cc_valid;
+ int db_exists;
+
+ if (xe == NULL){
+ clicon_err(OE_CFG, EINVAL, "xe is NULL");
+ goto done;
+ }
+ if (clicon_session_id_get(h, &session_id) < 0) {
+ clicon_err(OE_DAEMON, 0,
+ "an ephemeral confirmed-commit was issued, but the session-id could not be determined");
+ goto done;
+ };
+ /* 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.
+ */
+#ifdef _GLOBAL_VALID_CONFIRMING_COMMIT
+ cc_valid = _is_valid_confirming_commit;
+ // assert(cc_valid == check_valid_confirming_commit(h, xe, session_id));
+#else
+ cc_valid = check_valid_confirming_commit(h, xe, session_id);
+#endif
+ if (cc_valid) {
+ if (cancel_rollback_event(h) < 0) {
+ clicon_err(OE_DAEMON, 0, "A valid confirming-commit was received, but the corresponding rollback event was not found");
+ }
+
+ if (confirmed_commit_state_get(h) == PERSISTENT &&
+ confirmed_commit_persist_id_get(h) != NULL) {
+ confirmed_commit_persist_id_set(h, NULL);
+ }
+
+ confirmed_commit_state_set(h, INACTIVE);
+ }
+
+ /* Now, determine if there is a subsequent confirmed-commit */
+ if (xe_confirmed(xe)){
+ /* 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 */
+ confirm_timeout = xe_timeout(xe);
+ if (xe_persist(xe, &persist)){
+ if (persist == NULL) {
+ confirmed_commit_persist_id_set(h, NULL);
+ }
+ else if (confirmed_commit_persist_id_set(h, persist) < 0){
+ goto done;
+ }
+
+ /* The client has passed ; the confirming-commit MUST now be accompanied by a matching
+ *
+ */
+ confirmed_commit_state_set(h, 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_get(h), confirm_timeout);
+ }
+
+ else {
+ /* The client did not pass a value for and therefore any subsequent confirming-commit must be
+ * issued within the same session.
+ */
+ confirmed_commit_session_id_set(h, session_id);
+ confirmed_commit_state_set(h, 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_get(h),
+ 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.
+ *
+ */
+
+ 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 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.
+ *
+ * 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)
+ *
+ * @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)
+{
+ int retval = -1;
+ uint8_t errstate = 0;
+ 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_CRIT, "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_get(h) == PERSISTENT &&
+ confirmed_commit_persist_id_get(h) != NULL) {
+ confirmed_commit_persist_id_set(h, NULL);
+ }
+ confirmed_commit_state_set(h, ROLLBACK);
+ if (candidate_commit(h, NULL, "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_CRIT, "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;
+ };
+ retval = 0;
+ done:
+ confirmed_commit_state_set(h, INACTIVE);
+ if (errs)
+ *errs = errstate;
+ return retval;
+}
+
+/*! 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:
+ * @param[out] cbret Return xml tree, eg ..., ", NETCONF_BASE_NAMESPACE);
+ clicon_log(LOG_INFO, "a confirmed-commit has been cancelled by client request");
+ }
+ retval = 0;
+ done:
+ return retval;
+}
+
+/*! Incoming commit handler for confirmed commit
+ * @param[in] h Clicon handle
+ * @param[in] xe Request:
+ * @param[in] myid Client-id
+ * @param[out] cbret Return xml tree
+ * @retval 1 OK
+ * @retval 0 OK, dont proceed with commit
+ * @retval -1 Error
+ */
+int
+from_client_confirmed_commit(clicon_handle h,
+ cxobj *xe,
+ uint32_t myid,
+ cbuf *cbret)
+{
+ int retval = -1;
+ int cc_valid;
+
+ if ((cc_valid = check_valid_confirming_commit(h, xe, myid)) < 0)
+ goto done;
+#ifdef _GLOBAL_VALID_CONFIRMING_COMMIT
+ _is_valid_confirming_commit = cc_valid;
+#endif
+
+ /* If is *not* present, this will conclude the confirmed-commit, so cancel the rollback. */
+ if (!xe_confirmed(xe) && cc_valid) {
+ cancel_confirmed_commit(h);
+ cprintf(cbret, "", NETCONF_BASE_NAMESPACE);
+ goto dontcommit;
+ }
+ retval = 1;
+ done:
+ return retval;
+ dontcommit:
+ retval = 0;
+ goto done;
+}
diff --git a/apps/backend/backend_main.c b/apps/backend/backend_main.c
index 0b9a24ea..79b86ee2 100644
--- a/apps/backend/backend_main.c
+++ b/apps/backend/backend_main.c
@@ -124,6 +124,7 @@ backend_terminate(clicon_handle h)
xml_free(x);
if ((x = clicon_conf_xml(h)) != NULL)
xml_free(x);
+ confirmed_commit_free(h);
stream_publish_exit();
/* Delete all plugins, RPC callbacks, and upgrade callbacks */
clixon_plugin_module_exit(h);
@@ -154,10 +155,6 @@ 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() */
}
@@ -850,7 +847,11 @@ main(int argc,
if (clicon_option_bool(h, "CLICON_XML_CHANGELOG"))
if (clixon_xml_changelog_init(h) < 0)
goto done;
-
+ /* Init commit confirmed */
+ if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
+ if (confirmed_commit_init(h) < 0)
+ goto done;
+ }
/* Save modules state of the backend (server). Compare with startup XML */
if (startup_module_state(h, yspec) < 0)
goto done;
diff --git a/apps/backend/clixon_backend_commit.h b/apps/backend/clixon_backend_commit.h
index 46a00a64..639ae80d 100644
--- a/apps/backend/clixon_backend_commit.h
+++ b/apps/backend/clixon_backend_commit.h
@@ -52,36 +52,29 @@ enum confirmed_commit_state {
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; // XXX global
-
/*
* Prototypes
*/
-int do_rollback(clicon_handle h, uint8_t *errs);
-int cancel_rollback_event(void);
+/* backend_confirm.c */
+int confirmed_commit_init(clicon_handle h);
+int confirmed_commit_free(clicon_handle h);
+enum confirmed_commit_state confirmed_commit_state_get(clicon_handle h);
+uint32_t confirmed_commit_session_id_get(clicon_handle h);
+int cancel_rollback_event(clicon_handle h);
int cancel_confirmed_commit(clicon_handle h);
+int handle_confirmed_commit(clicon_handle h, cxobj *xe);
+int do_rollback(clicon_handle h, uint8_t *errs);
+int from_client_cancel_commit(clicon_handle h, cxobj *xe, cbuf *cbret, void *arg, void *regarg);
+int from_client_confirmed_commit(clicon_handle h, cxobj *xe, uint32_t myid, cbuf *cbret);
+/* backend_commit.c */
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);
-int candidate_commit(clicon_handle h, char *db, cbuf *cbret);
+int candidate_commit(clicon_handle h, cxobj *xe, char *db, cbuf *cbret);
int from_client_commit(clicon_handle h, cxobj *xe, cbuf *cbret, void *arg, void *regarg);
int from_client_discard_changes(clicon_handle h, cxobj *xe, cbuf *cbret, void *arg, void *regarg);
-int from_client_cancel_commit(clicon_handle h, cxobj *xe, cbuf *cbret, void *arg, void *regarg);
int from_client_validate(clicon_handle h, cxobj *xe, cbuf *cbret, void *arg, void *regarg);
int from_client_restart_one(clicon_handle h, clixon_plugin_t *cp, cbuf *cbret);
int load_failsafe(clicon_handle h, char *phase);
diff --git a/lib/clixon/clixon_data.h b/lib/clixon/clixon_data.h
index 376d8d0b..d0ae7dcb 100644
--- a/lib/clixon/clixon_data.h
+++ b/lib/clixon/clixon_data.h
@@ -66,6 +66,7 @@ int clicon_data_get(clicon_handle h, const char *name, char **val);
int clicon_data_set(clicon_handle h, const char *name, char *val);
int clicon_data_del(clicon_handle h, const char *name);
+/* Get generic clixon data on the form = where is void* */
int clicon_ptr_get(clicon_handle h, const char *name, void **ptr);
int clicon_ptr_set(clicon_handle h, const char *name, void *ptr);
int clicon_ptr_del(clicon_handle h, const char *name);
diff --git a/util/clixon_util_validate.c b/util/clixon_util_validate.c
index 5e9f2e5b..4ea77e60 100644
--- a/util/clixon_util_validate.c
+++ b/util/clixon_util_validate.c
@@ -227,7 +227,7 @@ main(int argc,
goto done;
}
if (commit){
- if ((ret = candidate_commit(h, database, cb)) < 0)
+ if ((ret = candidate_commit(h, NULL, database, cb)) < 0)
goto done;
}
else{