From 62e652bbcf2783e5f8fa6187f7b25eaec8955201 Mon Sep 17 00:00:00 2001 From: Olof hagsand Date: Wed, 19 Oct 2022 17:35:38 +0200 Subject: [PATCH] Confirm-commit refactoring Moved commit-confirm code to backend_confirm.c and removed (almost all) globals vars --- apps/backend/Makefile.in | 1 + apps/backend/backend_client.c | 12 +- apps/backend/backend_client.h | 2 +- apps/backend/backend_commit.c | 584 +------------------ apps/backend/backend_confirm.c | 834 +++++++++++++++++++++++++++ apps/backend/backend_main.c | 11 +- apps/backend/clixon_backend_commit.h | 31 +- lib/clixon/clixon_data.h | 1 + util/clixon_util_validate.c | 2 +- 9 files changed, 883 insertions(+), 595 deletions(-) create mode 100644 apps/backend/backend_confirm.c 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{