Initial implementation of NETCONF confirmed-commit

This commit is contained in:
Phil Heller 2022-09-30 19:17:13 -06:00
parent 954e5d56fd
commit 284316b646
16 changed files with 1375 additions and 26 deletions

View file

@ -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

View file

@ -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");

View file

@ -70,6 +70,15 @@
#include "backend_handle.h"
#include "clixon_backend_commit.h"
#include "backend_client.h"
#include "backend_failsafe.h"
/* a global instance of the confirmed_commit struct for reference throughout the procedure */
struct confirmed_commit confirmed_commit = {
.state = INACTIVE,
};
/* flag to carry indication if an RPC bearing <commit/> satisfies conditions to cancel the rollback timer */
static int is_valid_confirming_commit = 0;
/*! Key values are checked for validity independent of user-defined callbacks
*
@ -634,6 +643,240 @@ candidate_validate(clicon_handle h,
goto done;
}
/*! Cancel a scheduled rollback as previously registered by schedule_rollback_event()
*
* @retval 0 Rollback event successfully cancelled
* @retval -1 No Rollback event was found
*/
int
cancel_rollback_event()
{
int retval;
if ((retval = clixon_event_unreg_timeout(confirmed_commit.fn, confirmed_commit.arg)) == 0) {
clicon_log(LOG_INFO, "a scheduled rollback event has been cancelled");
} else {
clicon_log(LOG_WARNING, "the specified scheduled rollback event was not found");
}
return retval;
}
/*! Apply the rollback configuration upon expiration of the confirm-timeout
*
* @param[in] fd a dummy argument per the event callback semantics
* @param[in] arg a void pointer to a clicon_handle
* @retval 0 the rollback was successful
* @retval -1 the rollback failed
* @see do_rollback()
*/
static int
rollback_fn(int fd,
void *arg)
{
clicon_handle h = arg;
clicon_log(LOG_EMERG, "a confirming-commit was not received before the confirm-timeout expired; rolling back");
return do_rollback(h, NULL);
}
/*! Schedule a rollback in case no confirming-commit is received before the confirm-timeout
*
* @param[in] h a clicon handle
* @param[in] timeout a uint32 representing the number of seconds before the rollback event should fire
*
* @retval 0 Rollback event successfully scheduled
* @retval -1 Rollback event was not scheduled
*/
static int
schedule_rollback_event(clicon_handle h,
uint32_t timeout)
{
int retval = -1;
// register a new scheduled event
struct timeval t, t1;
if (gettimeofday(&t, NULL) < 0) {
clicon_err(OE_UNIX, 0, "failed to get time of day: %s", strerror(errno));
goto done;
};
t1.tv_sec = timeout; t1.tv_usec = 0;
timeradd(&t, &t1, &t);
/* The confirmed-commit is either:
* - ephemeral, and the client requesting the new confirmed-commit is on the same session, OR
* - persistent, and the client provided the persist-id in the new confirmed-commit
*/
/* remember the function pointer and args so the confirming-commit can cancel the rollback */
confirmed_commit.fn = rollback_fn;
confirmed_commit.arg = h;
if (clixon_event_reg_timeout(t, rollback_fn, h, "rollback after timeout") < 0) {
/* error is logged in called function */
goto done;
};
retval = 0;
done:
return retval;
}
/*! Handle the second phase of confirmed-commit processing.
*
* In the first phase, the proper action was taken in the case of a valid confirming-commit, but no subsequent
* confirmed-commit.
*
* In the second phase, the action taken is to handle both confirming- and confirmed-commit by creating the
* rollback database as required, then deleting it once the sequence is complete.
*
* @param[in] h Clicon handle
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @retval 0 OK
* @retval -1 Error
*/
static int
handle_confirmed_commit(clicon_handle h,
cbuf *cbret)
{
cxobj *persist_xml;
char *persist;
cxobj *confirm_timeout_xml;
char *confirm_timeout_str;
unsigned long confirm_timeout = 0L;
int retval = -1;
/* The case of a valid confirming-commit is also handled in the first phase, but only if there is no subsequent
* confirmed-commit. It is tested again here as the case of a valid confirming-commit *with* a subsequent
* confirmed-commit must be handled once the transaction has begun and after all the plugins' validate callbacks
* have been called.
*/
if (is_valid_confirming_commit) {
if (cancel_rollback_event() < 0) {
clicon_err(OE_DAEMON, 0, "A valid confirming-commit was received, but the corresponding rollback event was not found");
}
if (confirmed_commit.state == PERSISTENT && confirmed_commit.persist_id != NULL) {
free(confirmed_commit.persist_id);
confirmed_commit.persist_id = NULL;
}
confirmed_commit.state = INACTIVE;
}
/* Now, determine if there is a subsequent confirmed-commit */
if (xml_find_type(confirmed_commit.xe, NULL, "confirmed", CX_ELMNT) != NULL) {
/* There is, get it's confirm-timeout value, which will default per the yang schema if not client-specified */
/* Clixon also pre-validates input according to the schema, so bounds checking here is redundant */
if ((confirm_timeout_xml = xml_find_type(confirmed_commit.xe, NULL, "confirm-timeout", CX_ELMNT)) != NULL) {
if ((confirm_timeout_str = xml_body(confirm_timeout_xml)) == NULL) {
clicon_err(OE_DAEMON, 0, "%s: schema compliance error", __FUNCTION__);
goto done;
};
confirm_timeout = strtoul(confirm_timeout_str, NULL, 10);
}
if ((persist_xml = xml_find_type(confirmed_commit.xe, NULL, "persist", CX_ELMNT)) != NULL) {
/* an empty string is permitted, but it is represeted as NULL */
persist = xml_body(persist_xml);
if (persist == NULL) {
confirmed_commit.persist_id = NULL;
} else if ((confirmed_commit.persist_id = strdup4(persist)) == NULL) {
clicon_err(OE_UNIX, errno, "strdup4");
goto done;
}
/* The client has passed <persist>; the confirming-commit MUST now be accompanied by a matching
* <persist-id>
*/
confirmed_commit.state = PERSISTENT;
clicon_log(LOG_INFO,
"a persistent confirmed-commit has been requested with persist id of '%s' and a timeout of %lu seconds",
confirmed_commit.persist_id, confirm_timeout);
} else {
/* The client did not pass a value for <persist> and therefore any subsequent confirming-commit must be
* issued within the same session.
*/
if (clicon_session_id_get(h, &confirmed_commit.session_id) < 0) {
clicon_err(OE_DAEMON, 0,
"an ephemeral confirmed-commit was issued, but the session-id could not be determined");
if (netconf_operation_failed(cbret, "application",
"there was an error while performing the confirmed-commit") < 0)
clicon_err(OE_DAEMON, 0, "there was an error sending a netconf response to the client");
goto done;
};
confirmed_commit.state = EPHEMERAL;
clicon_log(LOG_INFO,
"an ephemeral confirmed-commit has been requested by session-id %u and a timeout of %lu seconds",
confirmed_commit.session_id, confirm_timeout);
}
/* The confirmed-commits and confirming-commits can overlap; the rollback database is created at the beginning
* of such a sequence and deleted at the end; hence its absence implies this is the first of a sequence. **
*
*
* | edit
* | | confirmed-commit
* | | copy t=0 running to rollback
* | | | edit
* | | | | both
* | | | | | edit
* | | | | | | both
* | | | | | | | confirming-commit
* | | | | | | | | delete rollback
* +----|-|-|-|-|-|-|-|---------------
* t=0 1 2 3 4 5 6 7 8
*
* edit = edit of the candidate configuration
* both = both a confirmed-commit and confirming-commit in the same RPC
*
* As shown, the rollback database created at t=2 is comprised of the running database from t=0
* Thus, if there is a rollback event at t=7, the t=0 configuration will be committed.
*
* ** the rollback database may be present at system startup if there was a crash during a confirmed-commit;
* in the case the system is configured to startup from running and the rollback database is present, the
* rollback database will be committed to running and then deleted. If the system is configured to use a
* startup configuration instead, any present rollback database will be deleted.
*
*/
int db_exists = xmldb_exists(h, "rollback");
if (db_exists == -1) {
clicon_err(OE_DAEMON, 0, "there was an error while checking existence of the rollback database");
goto done;
} else if (db_exists == 0) {
// db does not yet exists
if (xmldb_copy(h, "running", "rollback") < 0) {
clicon_err(OE_DAEMON, 0, "there was an error while copying the running configuration to rollback database.");
goto done;
};
}
if (schedule_rollback_event(h, confirm_timeout) < 0) {
clicon_err(OE_DAEMON, 0, "the rollback event could not be scheduled");
goto done;
};
} else {
/* There was no subsequent confirmed-commit, meaning this is the end of the confirmed/confirming sequence;
* The new configuration is already committed to running and the rollback database can now be deleted
*/
if (xmldb_delete(h, "rollback") < 0) {
clicon_err(OE_DB, 0, "Error deleting the rollback configuration");
goto done;
}
}
retval = 0;
done:
return retval;
}
/*! Do a diff between candidate and running, then start a commit transaction
*
* The code reverts changes if the commit fails. But if the revert
@ -654,6 +897,7 @@ candidate_commit(clicon_handle h,
transaction_data_t *td = NULL;
int ret;
cxobj *xret = NULL;
yang_stmt *yspec;
/* 1. Start transaction */
if ((td = transaction_new()) == NULL)
@ -664,6 +908,27 @@ candidate_commit(clicon_handle h,
*/
if ((ret = validate_common(h, db, td, &xret)) < 0)
goto done;
/* If the confirmed-commit feature is enabled, execute phase 2:
* - If a valid confirming-commit, cancel the rollback event
* - If a new confirmed-commit, schedule a new rollback event, otherwise
* - delete the rollback database
*
* Unless, however, this invocation of candidate_commit() was by way of a
* rollback event, in which case the timers are already cancelled and the
* caller will cleanup the rollback database. All that must be done here is
* to activate it.
*/
if ((yspec = clicon_dbspec_yang(h)) == NULL) {
clicon_err(OE_YANG, ENOENT, "No yang spec");
goto done;
}
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")
&& confirmed_commit.state != ROLLBACK
&& handle_confirmed_commit(h, cbret) < 0)
goto done;
if (ret == 0){
if (clixon_xml2cbuf(cbret, xret, 0, 0, -1, 0) < 0)
goto done;
@ -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 <commit/> must either:
* 1) be presented without a <persist-id> value, and on the same session as a prior confirmed-commit that itself was
* without a <persist> value, OR
* 2) be presented with a <persist-id> value that matches the <persist> value accompanying the prior confirmed-commit
*
* @param[in] h Clicon handle
* @param[in] myid current client session-id
* @param[out] cbret Return xml tree, eg <rpc-reply>..., <rpc-error..
* @retval 0 The confirming-commit is not valid
* @retval 1 The confirming-commit is valid
* @retval -1 Error
*/
static int
check_valid_confirming_commit(clicon_handle h,
uint32_t myid,
cbuf *cbret)
{
int retval = -1;
cxobj *persist_id_xml = NULL;
char *persist_id = NULL;
if (confirmed_commit.xe == NULL) {
retval = 0;
goto done;
}
switch (confirmed_commit.state) {
case PERSISTENT:
if ((persist_id_xml = xml_find_type(confirmed_commit.xe, NULL, "persist-id", CX_ELMNT)) != NULL) {
persist_id = xml_body(persist_id_xml);
if ((persist_id == NULL && confirmed_commit.persist_id == NULL) || // empty strings deserialized as NULL
(persist_id != NULL && confirmed_commit.persist_id != NULL &&
strcmp(persist_id, confirmed_commit.persist_id) == 0)) {
/* the RPC included a <persist-id> matching the prior confirming-commit's <persist> */
retval = 1;
break;
} else {
netconf_invalid_value(cbret, "protocol", "No such persist-id");
clicon_log(LOG_INFO,
"a persistent confirmed-commit is in progress but the client issued a "
"confirming-commit with an incorrect persist-id");
retval = 0;
break;
}
} else {
netconf_invalid_value(cbret, "protocol", "Persist-id not given");
clicon_log(LOG_INFO,
"a persistent confirmed-commit is in progress but the client issued a confirming-commit"
"without a persist-id");
retval = 0;
break;
}
case EPHEMERAL:
if (myid == confirmed_commit.session_id) {
/* the RPC lacked a <persist-id>, the prior confirming-commit lacked <persist>, and both were issued
* on the same session.
*/
retval = 1;
break;
}
if (netconf_invalid_value(cbret, "protocol","confirming-commit not performed on originating session") < 0) {
clicon_err(OE_NETCONF, 0, "error sending response");
break;
};
clicon_log(LOG_DEBUG, "an ephemeral confirmed-commit is in progress, but there confirming-commit was"
"not issued on the same session as the confirmed-commit");
retval = 0;
break;
default:
clicon_debug(1, "commit-confirmed state !? %d", confirmed_commit.state);
retval = 0;
break;
}
done:
return retval;
}
/*! Commit the candidate configuration as the device's new current configuration
*
* @param[in] h Clicon handle
@ -747,6 +1183,52 @@ from_client_commit(clicon_handle h,
uint32_t iddb;
cbuf *cbx = NULL; /* Assist cbuf */
int ret;
yang_stmt *yspec;
/* Handle the first phase of confirmed-commit
*
* First, it must be determined if the given <commit> RPC constitutes a "confirming-commit", roughly meaning:
* 1) it was issued in the same session as a prior confirmed-commit
* 2) it bears a <persist-id> element matching the <persist> element that accompanied the prior confirmed-commit
*
* If it is a valid "confirming-commit" and this RPC does not bear another <confirmed/> element, then the
* confirmed-commit is complete, the rollback event can be cancelled and the rollback database deleted.
*
* No further action is necessary as the candidate configuration was already copied to the running configuration.
*
* If the RPC does bear another <confirmed/> element, that will be handled in phase two, from within the
* candidate_commit() method.
*/
if ((yspec = clicon_dbspec_yang(h)) == NULL) {
clicon_err(OE_YANG, ENOENT, "No yang spec");
goto done;
}
if (if_feature(yspec, "ietf-netconf", "confirmed-commit")) {
confirmed_commit.xe = xe;
if ((is_valid_confirming_commit = check_valid_confirming_commit(h, myid, cbret)) < 0)
goto done;
/* If <confirmed/> is *not* present, this will conclude the confirmed-commit, so cancel the rollback. */
if (xml_find_type(confirmed_commit.xe, NULL, "confirmed", CX_ELMNT) == NULL
&& is_valid_confirming_commit) {
cancel_rollback_event();
if (confirmed_commit.state == PERSISTENT && confirmed_commit.persist_id != NULL) {
free(confirmed_commit.persist_id);
confirmed_commit.persist_id = NULL;
}
confirmed_commit.state = INACTIVE;
if (xmldb_delete(h, "rollback") < 0)
clicon_err(OE_DB, 0, "Error deleting the rollback configuration");
cprintf(cbret, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", NETCONF_BASE_NAMESPACE);
goto ok;
}
}
/* Check if target locked by other client */
iddb = xmldb_islocked(h, "running");
@ -772,6 +1254,7 @@ from_client_commit(clicon_handle h,
ok:
retval = 0;
done:
confirmed_commit.xe = NULL;
if (cbx)
cbuf_free(cbx);
return retval; /* may be zero if we ignoring errors from commit */
@ -832,6 +1315,8 @@ from_client_discard_changes(clicon_handle h,
/*! Cancel an ongoing confirmed commit.
* If the confirmed commit is persistent, the parameter 'persist-id' must be
* given, and it must match the value of the 'persist' parameter.
* If the confirmed-commit is ephemeral, the 'persist-id' must not be given and both the confirmed-commit and the
* cancel-commit must originate from the same session.
*
* @param[in] h Clicon handle
* @param[in] xe Request: <rpc><xn></rpc>
@ -850,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, "<rpc-reply xmlns=\"%s\"><ok/></rpc-reply>", 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.

View file

@ -0,0 +1,105 @@
/*
*
***** BEGIN LICENSE BLOCK *****
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
Copyright (C) 2017-2019 Olof Hagsand
Copyright (C) 2020-2021 Olof Hagsand and Rubicon Communications, LLC(Netgate)
This file is part of CLIXON.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Alternatively, the contents of this file may be used under the terms of
the GNU General Public License Version 3 or later (the "GPL"),
in which case the provisions of the GPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of the GPL, and not to allow others to
use your version of this file under the terms of Apache License version 2,
indicate your decision by deleting the provisions above and replace them with
the notice and other provisions required by the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the Apache License version 2 or the GPL.
***** END LICENSE BLOCK *****
*/
#ifdef HAVE_CONFIG_H
#include "clixon_config.h" /* generated by config & autoconf */
#endif
#include <stdlib.h>
#include <errno.h>
#include <syslog.h>
/* cligen */
#include <cligen/cligen.h>
/* clixon */
#include <clixon/clixon.h>
#include "clixon_backend_commit.h"
#include "backend_failsafe.h"
/*! Reset running and start in failsafe mode. If no failsafe then quit.
Typically done when startup status is not OK so
failsafe ----------------------+
reset \ commit
running ----|-------+---------------> RUNNING FAILSAFE
\
tmp |---------------------->
*/
int
load_failsafe(clicon_handle h, char *phase)
{
int retval = -1;
int ret;
char *db = "failsafe";
cbuf *cbret = NULL;
phase = phase == NULL ? "(unknown)" : phase;
if ((cbret = cbuf_new()) == NULL){
clicon_err(OE_XML, errno, "cbuf_new");
goto done;
}
if ((ret = xmldb_exists(h, db)) < 0)
goto done;
if (ret == 0){ /* No it does not exist, fail */
clicon_err(OE_DB, 0, "%s failed and no Failsafe database found, exiting", phase);
goto done;
}
/* Copy original running to tmp as backup (restore if error) */
if (xmldb_copy(h, "running", "tmp") < 0)
goto done;
if (xmldb_db_reset(h, "running") < 0)
goto done;
ret = candidate_commit(h, db, cbret);
if (ret != 1)
if (xmldb_copy(h, "tmp", "running") < 0)
goto done;
if (ret < 0)
goto done;
if (ret == 0){
clicon_err(OE_DB, 0, "%s failed, Failsafe database validation failed %s", phase, cbuf_get(cbret));
goto done;
}
clicon_log(LOG_NOTICE, "%s failed, Failsafe database loaded ", phase);
retval = 0;
done:
if (cbret)
cbuf_free(cbret);
return retval;
}

View file

@ -0,0 +1,42 @@
/*
*
***** BEGIN LICENSE BLOCK *****
Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren
Copyright (C) 2017-2019 Olof Hagsand
Copyright (C) 2020-2021 Olof Hagsand and Rubicon Communications, LLC(Netgate)
This file is part of CLIXON.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Alternatively, the contents of this file may be used under the terms of
the GNU General Public License Version 3 or later (the "GPL"),
in which case the provisions of the GPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of the GPL, and not to allow others to
use your version of this file under the terms of Apache License version 2,
indicate your decision by deleting the provisions above and replace them with
the notice and other provisions required by the GPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the Apache License version 2 or the GPL.
***** END LICENSE BLOCK *****
*/
#ifndef CLIXON_BACKEND_FAILSAFE_H
#define CLIXON_BACKEND_FAILSAFE_H
int load_failsafe(clicon_handle h, char *phase);
#endif //CLIXON_BACKEND_FAILSAFE_H

View file

@ -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;

View file

@ -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;

View file

@ -39,9 +39,40 @@
#ifndef _CLIXON_BACKEND_COMMIT_H_
#define _CLIXON_BACKEND_COMMIT_H_
#define ROLLBACK_NOT_APPLIED 1
#define ROLLBACK_DB_NOT_DELETED 2
#define ROLLBACK_FAILSAFE_APPLIED 4
#define COMMIT_NOT_CONFIRMED "Commit was not confirmed; automatic rollback complete."
enum confirmed_commit_state {
INACTIVE, // a confirmed-commit is not in progress
PERSISTENT, // a confirmed-commit is in progress and a persist value was given
EPHEMERAL, // a confirmed-commit is in progress and a persist value was not given
ROLLBACK
};
/* A struct to store the information necessary for tracking the status and relevant details of
* one or more overlapping confirmed-commit events.
*/
struct confirmed_commit {
enum confirmed_commit_state state;
char *persist_id; // a value given by a client in the confirmed-commit
uint32_t session_id; // the session_id of the client that gave no <persist> value
cxobj *xe; // the commit confirmed request
int (*fn)(int, void*); // the function pointer for the rollback event (rollback_fn())
void *arg; // the clicon_handle that will be passed to rollback_fn()
};
extern struct confirmed_commit confirmed_commit;
/*
* Prototypes
*/
*/
int do_rollback(clicon_handle h, uint8_t *errs);
int cancel_rollback_event();
int startup_validate(clicon_handle h, char *db, cxobj **xtr, cbuf *cbret);
int startup_commit(clicon_handle h, char *db, cbuf *cbret);
int candidate_validate(clicon_handle h, char *db, cbuf *cbret);

View file

@ -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:

View file

@ -1,6 +1,7 @@
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>/usr/local/etc/example.xml</CLICON_CONFIGFILE>
<CLICON_FEATURE>ietf-netconf:startup</CLICON_FEATURE>
<CLICON_FEATURE>ietf-netconf:confirmed-commit</CLICON_FEATURE>
<CLICON_FEATURE>clixon-restconf:allow-auth-none</CLICON_FEATURE>
<CLICON_FEATURE>clixon-restconf:fcgi</CLICON_FEATURE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>

View file

@ -48,7 +48,15 @@ delete("Delete a configuration item") {
all("Delete whole candidate configuration"), delete_all("candidate");
}
validate("Validate changes"), cli_validate();
commit("Commit the changes"), cli_commit();
commit("Commit the changes"), cli_commit(); {
[persist-id("Specify the 'persist' value of a previous confirmed-commit") <persist-id-val:string show:"string">("The 'persist' value of the persistent confirmed-commit")], cli_commit(); {
<cancel:string keyword:cancel>("Cancel an ongoing confirmed-commit"), cli_commit();
<confirmed:string keyword:confirmed>("Require a confirming commit") {
[persist("Make this confirmed-commit persistent") <persist-val:string show:"string">("The value that must be provided as 'persist-id' in the confirming-commit or cancel-commit")]
[<timeout:uint32 range[1:4294967295] show:"1..4294967295">("The rollback timeout in seconds")], cli_commit();
}
}
}
quit("Quit"), cli_quit();
debug("Debugging parts of the system"){

View file

@ -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 */

View file

@ -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);

View file

@ -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;
}

View file

@ -83,6 +83,10 @@
#include "clixon_netconf_lib.h"
#include "clixon_proto_client.h"
#define PERSIST_ID_XML_FMT "<persist-id>%s</persist-id>"
#define PERSIST_XML_FMT "<persist>%s</persist>"
#define TIMEOUT_XML_FMT "<confirm-timeout>%u</confirm-timeout>"
/*! Connect to internal netconf socket
*/
int
@ -1266,7 +1270,12 @@ clicon_rpc_validate(clicon_handle h,
* @retval -1 Error and logged to syslog
*/
int
clicon_rpc_commit(clicon_handle h)
clicon_rpc_commit(clicon_handle h,
int confirmed,
int cancel,
uint32_t timeout,
char *persist,
char *persist_id)
{
int retval = -1;
struct clicon_msg *msg = NULL;
@ -1274,15 +1283,65 @@ clicon_rpc_commit(clicon_handle h)
cxobj *xerr;
char *username;
uint32_t session_id;
char *persist_id_xml = NULL;
char *persist_xml = NULL;
char *timeout_xml = NULL;
if (persist_id) {
if ((persist_id_xml = malloc(strlen(persist_id) + strlen(PERSIST_ID_XML_FMT) + 1)) == NULL) {
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
}
sprintf(persist_id_xml, PERSIST_ID_XML_FMT, persist_id);
}
if (persist) {
if ((persist_xml = malloc(strlen(persist) + strlen(PERSIST_XML_FMT) + 1)) == NULL) {
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
};
sprintf(persist_xml, PERSIST_XML_FMT, persist);
}
if (timeout > 0) {
/* timeout is a uint32_t, so max value is 2^32, a 10-digit number, we'll just always malloc for a string that
* may be that large rather than calculate the string length
*/
if ((timeout_xml = malloc(10 + 1 + strlen(TIMEOUT_XML_FMT))) == NULL) {
clicon_err(OE_UNIX, 0, "malloc: %s", strerror(errno));
};
sprintf(timeout_xml, TIMEOUT_XML_FMT, timeout);
}
if (session_id_check(h, &session_id) < 0)
goto done;
username = clicon_username_get(h);
if ((msg = clicon_msg_encode(session_id,
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit/></rpc>",
NETCONF_BASE_NAMESPACE,
username?username:"",
NETCONF_MESSAGE_ID_ATTR)) == NULL)
if (cancel) {
msg = clicon_msg_encode(session_id,
"<rpc xmlns=\"%s\" username=\"%s\" %s><cancel-commit>%s</cancel-commit></rpc>",
NETCONF_BASE_NAMESPACE,
username ? username : "",
NETCONF_MESSAGE_ID_ATTR,
persist_id ? persist_id_xml : "");
} else if (confirmed) {
msg = clicon_msg_encode(session_id,
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit><confirmed/>%s%s%s</commit></rpc>",
NETCONF_BASE_NAMESPACE,
username ? username : "",
NETCONF_MESSAGE_ID_ATTR,
timeout ? timeout_xml : "",
persist_id ? persist_id_xml : "",
persist ? persist_xml : "");
} else {
msg = clicon_msg_encode(session_id,
"<rpc xmlns=\"%s\" username=\"%s\" %s><commit>%s</commit></rpc>",
NETCONF_BASE_NAMESPACE,
username ? username : "",
NETCONF_MESSAGE_ID_ATTR,
persist ? persist_xml : "");
}
if (msg == NULL)
goto done;
if (clicon_rpc_msg(h, msg, &xret) < 0)
goto done;
@ -1296,6 +1355,12 @@ clicon_rpc_commit(clicon_handle h)
xml_free(xret);
if (msg)
free(msg);
if (persist_id_xml)
free(persist_id_xml);
if (persist_xml)
free(persist_xml);
if (timeout_xml)
free(timeout_xml);
return retval;
}
@ -1479,7 +1544,7 @@ clicon_rpc_restconf_debug(clicon_handle h,
clicon_err(OE_XML, 0, "rpc error"); /* XXX extract info from rpc-error */
goto done;
}
if ((retval = clicon_rpc_commit(h)) < 0)
if ((retval = clicon_rpc_commit(h, 0, 0, 0, NULL, NULL)) < 0)
goto done;
done:
if (msg)

355
test/test_confirmed_commit.sh Executable file
View file

@ -0,0 +1,355 @@
#!/usr/bin/env bash
# Basic Netconf functionality
# Mainly default/null prefix, but also xx: prefix
# XXX: could add tests for dual prefixes xx and xy with doppelganger names, ie xy:filter that is
# syntactic correct but wrong
# Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
APPNAME=example
cfg=$dir/conf_yang.xml
tmp=$dir/tmp.x
fyang=$dir/clixon-example.yang
# Use yang in example
cat <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_FEATURE>ietf-netconf:startup</CLICON_FEATURE>
<CLICON_FEATURE>ietf-netconf:confirmed-commit</CLICON_FEATURE>
<CLICON_MODULE_SET_ID>42</CLICON_MODULE_SET_ID>
<CLICON_YANG_DIR>${YANG_INSTALLDIR}</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<CLICON_YANG_MAIN_FILE>$fyang</CLICON_YANG_MAIN_FILE>
<CLICON_CLISPEC_DIR>/usr/local/lib/$APPNAME/clispec</CLICON_CLISPEC_DIR>
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
<CLICON_BACKEND_REGEXP>example_backend.so$</CLICON_BACKEND_REGEXP>
<CLICON_NETCONF_DIR>/usr/local/lib/$APPNAME/netconf</CLICON_NETCONF_DIR>
<CLICON_NETCONF_MESSAGE_ID_OPTIONAL>false</CLICON_NETCONF_MESSAGE_ID_OPTIONAL>
<CLICON_RESTCONF_DIR>/usr/local/lib/$APPNAME/restconf</CLICON_RESTCONF_DIR>
<CLICON_CLI_DIR>/usr/local/lib/$APPNAME/cli</CLICON_CLI_DIR>
<CLICON_CLI_MODE>$APPNAME</CLICON_CLI_MODE>
<CLICON_SOCK>$dir/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
</clixon-config>
EOF
cat <<EOF > $fyang
module clixon-example{
yang-version 1.1;
namespace "urn:example:clixon";
prefix ex;
import ietf-interfaces {
prefix if;
}
import ietf-ip {
prefix ip;
}
/* Example interface type for tests, local callbacks, etc */
identity eth {
base if:interface-type;
}
/* Generic config data */
container table{
list parameter{
key name;
leaf name{
type string;
}
}
}
/* State data (not config) for the example application*/
container state {
config false;
description "state data for the example application (must be here for example get operation)";
leaf-list op {
type string;
}
}
augment "/if:interfaces/if:interface" {
container my-status {
config false;
description "For testing augment+state";
leaf int {
type int32;
}
leaf str {
type string;
}
}
}
rpc client-rpc {
description "Example local client-side RPC that is processed by the
the netconf/restconf and not sent to the backend.
This is a clixon implementation detail: some rpc:s
are better processed by the client for API or perf reasons";
input {
leaf x {
type string;
}
}
output {
leaf x {
type string;
}
}
}
rpc empty {
description "Smallest possible RPC with no input or output sections";
}
rpc example {
description "Some example input/output for testing RFC7950 7.14.
RPC simply echoes the input for debugging.";
input {
leaf x {
description
"If a leaf in the input tree has a 'mandatory' statement with
the value 'true', the leaf MUST be present in an RPC invocation.";
type string;
mandatory true;
}
leaf y {
description
"If a leaf in the input tree has a 'mandatory' statement with the
value 'true', the leaf MUST be present in an RPC invocation.";
type string;
default "42";
}
}
output {
leaf x {
type string;
}
leaf y {
type string;
}
}
}
}
EOF
function data() {
if [[ "$1" == "" ]]
then
echo "<data/>"
else
echo "<data>$1</data>"
fi
}
# Pipe stdin to command and also do chunked framing (netconf 1.1)
# Arguments:
# - Command
# - expected command return value (0 if OK)
# - stdin input1 This is NOT encoded, eg preamble/hello
# - stdin input2 This gets chunked encoding
# - expect1 stdout outcome, can be partial and contain regexps
# - expect2 stdout outcome This gets chunked encoding, must be complete netconf message
# Use this if you want regex eg ^foo$
function rpc() {
expecteof_netconf "$clixon_netconf -qf $cfg" 0 "$DEFAULTHELLO" "<rpc $DEFAULTNS>$1</rpc>" "" "<rpc-reply $DEFAULTNS>$2</rpc-reply>"
}
function commit() {
if [[ "$1" == "" ]]
then
rpc "<commit/>" "<ok/>"
else
rpc "<commit>$1</commit>" "<ok/>"
fi
}
function edit_config() {
TARGET="$1"
CONFIG="$2"
rpc "<edit-config><target><$TARGET/></target><config>$CONFIG</config></edit-config>" "<ok/>"
}
function assert_config_equals() {
TARGET="$1"
EXPECTED="$2"
rpc "<get-config><source><$TARGET/></source></get-config>" "$(data "$EXPECTED")"
}
function reset() {
rpc "<edit-config><target><candidate/></target><default-operation>none</default-operation><config operation=\"delete\"/></edit-config>" "<ok/>"
commit
assert_config_equals "candidate" ""
assert_config_equals "running" ""
}
CANDIDATE_PATH="/usr/local/var/$APPNAME/candidate_db"
RUNNING_PATH="/usr/local/var/$APPNAME/running_db"
ROLLBACK_PATH="/usr/local/var/$APPNAME/rollback_db"
FAILSAFE_PATH="/usr/local/var/$APPNAME/failsafe_db"
CONFIGB="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth0</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
CONFIGC="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth1</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
CONFIGBPLUSC="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth0</name><type>ex:eth</type><enabled>true</enabled></interface><interface><name>eth1</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
FAILSAFE_CFG="<interfaces xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\"><interface><name>eth99</name><type>ex:eth</type><enabled>true</enabled></interface></interfaces>"
# TODO this test suite is somewhat brittle as it relies on the presence of the example configuration that one gets with
# make install-example in the Clixon distribution. It would be better if the dependencies were entirely self contained.
new "test params: -f $cfg -- -s"
# Bring your own backend
if [ $BE -ne 0 ]; then
# kill old backend (if any)
new "kill old backend"
sudo clixon_backend -zf $cfg
if [ $? -ne 0 ]; then
err
fi
new "start backend -s init -f $cfg -- -s"
start_backend -s init -f $cfg -- -s
fi
new "wait backend"
wait_backend
new "netconf ephemeral confirmed-commit rolls back after disconnect"
reset
edit_config "candidate" "$CONFIGB"
assert_config_equals "candidate" "$CONFIGB"
commit "<confirmed/><confirm-timeout>30</confirm-timeout>"
assert_config_equals "running" ""
new "netconf persistent confirmed-commit"
reset
edit_config "candidate" "$CONFIGB"
commit "<confirmed/><persist>a</persist>"
assert_config_equals "running" "$CONFIGB"
edit_config "candidate" "$CONFIGC"
commit "<confirmed/><persist>ab</persist><persist-id>a</persist-id>"
assert_config_equals "running" "$CONFIGBPLUSC"
new "netconf cancel-commit with invalid persist-id"
rpc "<cancel-commit><persist-id>abc</persist-id></cancel-commit>" "<rpc-error><error-type>application</error-type><error-tag>invalid-value</error-tag><error-severity>error</error-severity><error-message>a confirmed-commit with the given persist-id was not found</error-message></rpc-error>"
new "netconf cancel-commit with valid persist-id"
rpc "<cancel-commit><persist-id>ab</persist-id></cancel-commit>" "<ok/>"
new "netconf persistent confirmed-commit with timeout"
reset
edit_config "candidate" "$CONFIGB"
commit "<confirmed/><confirm-timeout>2</confirm-timeout><persist>abcd</persist>"
assert_config_equals "running" "$CONFIGB"
sleep 2
assert_config_equals "running" ""
new "netconf persistent confirmed-commit with reset timeout"
reset
edit_config "candidate" "$CONFIGB"
commit "<confirmed/><persist>abcde</persist><confirm-timeout>5</confirm-timeout>"
assert_config_equals "running" "$CONFIGB"
edit_config "candidate" "$CONFIGC"
commit "<confirmed/><persist-id>abcde</persist-id><persist>abcdef</persist><confirm-timeout>10</confirm-timeout>"
# prove the new timeout is active by sleeping longer than first timeout. get config, assert == B+C
sleep 6
assert_config_equals "running" "$CONFIGBPLUSC"
# now sleep long enough for rollback to happen; get config, assert == A
sleep 5
assert_config_equals "running" ""
new "netconf persistent confirming-commit to epehemeral confirmed-commit should rollback"
reset
edit_config "candidate" "$CONFIGB"
commit "<confirmed/><persist/><confirm-timeout>10</confirm-timeout>"
assert_config_equals "running" "$CONFIGB"
commit "<confirmed/><persist-id/>"
assert_config_equals "running" ""
new "netconf confirming-commit for persistent confirmed-commit with empty persist value"
reset
edit_config "candidate" "$CONFIGB"
commit "<confirmed/><persist/><confirm-timeout>10</confirm-timeout>"
assert_config_equals "running" "$CONFIGB"
commit "<persist-id/>"
assert_config_equals "running" "$CONFIGB"
# TODO the next two tests are broken. The whole idea of presence or absence of rollback_db indicating something might
# need reconsideration. see clixon_datastore.c#xmldb_delete() and backend_startup.c#startup_mode_startup()
new "backend loads rollback if present at startup"
reset
edit_config "candidate" "$CONFIGB"
commit ""
edit_config "candidate" "$CONFIGC"
commit "<persist>abcdefg</persist><confirmed/>"
assert_config_equals "running" "$CONFIGBPLUSC"
stop_backend -f $cfg # kill backend and restart
[ -f "$ROLLBACK_PATH" ] || err "rollback_db doesn't exist!" # assert rollback_db exists
start_backend -s running -f $cfg -- -s
wait_backend
assert_config_equals "running" "$CONFIGB"
[ -f "ROLLBACK_PATH" ] && err "rollback_db still exists!" # assert rollback_db doesn't exist
stop_backend -f $cfg
start_backend -s init -f $cfg -- -s
new "backend loads failsafe at startup if rollback present but cannot be loaded"
reset
sudo tee "$FAILSAFE_PATH" > /dev/null << EOF # create a failsafe database
<config>$FAILSAFE_CFG</config>
EOF
edit_config "candidate" "$CONFIGC"
commit "<persist>foobar</persist><confirmed/>"
assert_config_equals "running" "$CONFIGC"
stop_backend -f $cfg # kill the backend
sudo rm $ROLLBACK_PATH # modify rollback_db so it won't commit successfully
sudo tee "$ROLLBACK_PATH" > /dev/null << EOF
<foo>
<bar>
<baz/>
</bar>
</foo>
EOF
start_backend -s running -f $cfg -- -s
wait_backend
assert_config_equals "running" "$FAILSAFE_CFG"
# TODO this test is now broken too, but not sure why; suspicion that the initial confirmed-commit session is not kept alive as intended
stop_backend -f $cfg
start_backend -s init -f $cfg -lf/tmp/clixon.log -D1 -- -s
wait_backend
new "ephemeral confirmed-commit survives unrelated ephemeral session disconnect"
reset
edit_config "candidate" "$CONFIGB"
# start a new ephemeral confirmed commit, but keep the confirmed-commit session alive (need to put it in the background)
sleep 60 | cat <(echo "$DEFAULTHELLO<rpc $DEFAULTNS><commit><confirmed/><confirm-timeout>60</confirm-timeout></commit></rpc>]]>]]>") -| $clixon_netconf -qf $cfg >> /dev/null &
PIDS=($(jobs -l % | cut -c 6- | awk '{print $1}'))
assert_config_equals "running" "$CONFIGB" # assert config twice to prove it surives disconnect
assert_config_equals "running" "$CONFIGB" # of ephemeral sessions
kill -9 ${PIDS[0]} # kill the while loop above to close STDIN on 1st
# ephemeral session and cause rollback
assert_config_equals "running" ""
# TODO test same cli methods as tested for netconf
# TODO test restconf receives "409 conflict" when there is a persistent confirmed-commit active
# TODO test restconf causes confirming-commit for ephemeral confirmed-commit
if [ $BE -ne 0 ]; then
new "Kill backend"
# Check if premature kill
pid=$(pgrep -u root -f clixon_backend)
if [ -z "$pid" ]; then
err "backend already dead"
fi
# kill backend
stop_backend -f $cfg
fi
new "endtest"
endtest