From 284316b6465af35aa05c47d953464b20054de275 Mon Sep 17 00:00:00 2001 From: Phil Heller Date: Fri, 30 Sep 2022 19:17:13 -0600 Subject: [PATCH] Initial implementation of NETCONF confirmed-commit --- apps/backend/Makefile.in | 8 +- apps/backend/backend_client.c | 49 ++- apps/backend/backend_commit.c | 571 ++++++++++++++++++++++++++- apps/backend/backend_failsafe.c | 105 +++++ apps/backend/backend_failsafe.h | 42 ++ apps/backend/backend_main.c | 7 + apps/backend/backend_startup.c | 41 +- apps/backend/clixon_backend_commit.h | 33 +- apps/cli/cli_common.c | 17 +- example/main/example.xml.in | 1 + example/main/example_cli.cli | 10 +- lib/clixon/clixon_datastore.h | 1 + lib/clixon/clixon_proto_client.h | 3 +- lib/src/clixon_datastore.c | 77 +++- lib/src/clixon_proto_client.c | 81 +++- test/test_confirmed_commit.sh | 355 +++++++++++++++++ 16 files changed, 1375 insertions(+), 26 deletions(-) create mode 100644 apps/backend/backend_failsafe.c create mode 100644 apps/backend/backend_failsafe.h create mode 100755 test/test_confirmed_commit.sh 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.xml ietf-netconf:startup + ietf-netconf:confirmed-commit clixon-restconf:allow-auth-none clixon-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