diff --git a/modules/ietf-netconf-private-candidate@2026-02-03.yang b/modules/ietf-netconf-private-candidate@2026-02-03.yang
new file mode 100644
index 00000000..ccbb6dc7
--- /dev/null
+++ b/modules/ietf-netconf-private-candidate@2026-02-03.yang
@@ -0,0 +1,111 @@
+module ietf-netconf-private-candidate {
+ yang-version 1.1;
+ namespace "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate";
+ prefix pc;
+
+
+ organization
+ "IETF NETCONF (Network Configuration) Working Group";
+ contact
+ "WG Web:
+ WG List:
+
+ Editor: James Cumming
+
+
+ Editor: Robert Wills
+ ";
+ description
+ "NETCONF private candidate support.
+
+ Copyright (c) 2026 IETF Trust and the persons identified as
+ authors of the code. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or
+ without modification, is permitted pursuant to, and subject to
+ the license terms contained in, the Revised BSD License set
+ forth in Section 4.c of the IETF Trust's Legal Provisions
+ Relating to IETF Documents
+ (https://trustee.ietf.org/license-info).
+
+ This version of this YANG module is part of RFC XXXX
+ (https://www.rfc-editor.org/info/rfcXXXX); see the RFC itself
+ for full legal notices.
+
+ The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL
+ NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED',
+ 'MAY', and 'OPTIONAL' in this document are to be interpreted as
+ described in BCP 14 (RFC 2119) (RFC 8174) when, and only when,
+ they appear in all capitals, as shown here.";
+
+ revision 2026-02-03 {
+ description
+ "Introduce private candidate support";
+ reference
+ "draft-ietf-netconf-privcand:
+ Netconf Private Candidates";
+ }
+ revision 2025-10-30 {
+ description
+ "Introduce private candidate support";
+ reference
+ "draft-ietf-netconf-privcand:
+ Netconf Private Candidates";
+ }
+ revision 2024-09-12 {
+ description
+ "Introduce private candidate support";
+ reference
+ "draft-ietf-netconf-privcand:
+ Netconf Private Candidates";
+ }
+
+ feature private-candidate {
+ description
+ "NETCONF :private-candidate capability;
+ If the server advertises the :private-candidate
+ capability for a session, then this feature must
+ also be enabled for that session. Otherwise,
+ this feature must not be enabled.";
+ reference
+ "draft-ietf-netconf-privcand";
+ }
+
+ rpc update {
+ if-feature "private-candidate";
+ description
+ "Updates the private candidate from the running
+ configuration.";
+ reference
+ "draft-ietf-netconf-privcand";
+ input {
+ leaf resolution-mode {
+ type enumeration {
+ enum revert-on-conflict {
+ description
+ "Reject update when any conflicting
+ node is found and revert the private
+ candidate configuration datastore to its
+ state prior to issuing the update.";
+ }
+ enum prefer-candidate {
+ description
+ "Resolve conflicted node by selecting
+ the private candidate configuration
+ datastore version.";
+ }
+ enum prefer-running {
+ description
+ "Resolve conflicted node by selecting
+ the running configuration datastore
+ version.";
+ }
+ }
+ default "revert-on-conflict";
+ description
+ "Mode to resolve conflicts between running and
+ private-candidate configurations.";
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/netopeer-notifications@2026-01-05.yang b/modules/netopeer-notifications@2026-01-05.yang
index f4e5e86d..31f5ab9f 100644
--- a/modules/netopeer-notifications@2026-01-05.yang
+++ b/modules/netopeer-notifications@2026-01-05.yang
@@ -193,6 +193,11 @@ module netopeer-notifications {
description
"ietf-yang-push resync-subscription RPC";
}
+
+ enum update {
+ description
+ "ietf-netconf-private-candidate update RPC";
+ }
}
mandatory true;
description
diff --git a/scripts/common.sh b/scripts/common.sh
index a3cc47d8..3d4b2ec1 100644
--- a/scripts/common.sh
+++ b/scripts/common.sh
@@ -15,6 +15,7 @@ NP2_MODULES=(
"netopeer-notifications@2026-01-05.yang"
"ietf-system-capabilities@2022-02-17.yang"
"ietf-notification-capabilities@2022-02-17.yang"
+"ietf-netconf-private-candidate@2026-02-03.yang -e private-candidate"
)
LN2_MODULES=(
diff --git a/src/common.c b/src/common.c
index 1a9d7ae0..d0c60797 100644
--- a/src/common.c
+++ b/src/common.c
@@ -41,6 +41,7 @@
#include
#include
+#include
#include
#include
#include
@@ -740,6 +741,9 @@ np_new_session_cb(const char *UNUSED(client_name), struct nc_session *new_sessio
goto error;
}
+ user_sess->use_private_cand = nc_session_cpblt(new_session,
+ "urn:ietf:params:netconf:capability:private-candidate:1.0") ? 1 : 0;
+
/* generate ietf-netconf-notification's netconf-session-start event for sysrepo */
np_send_notif_session_start(new_session, np2srv.sr_sess, np2srv.sr_timeout);
@@ -1240,8 +1244,8 @@ np_op_filter_data_ignored_mod(struct lyd_node **data, const char *ignored_mod)
}
struct nc_server_reply *
-np_op_filter_data_get(sr_session_ctx_t *session, uint32_t max_depth, uint32_t get_opts, const char *xp_filter,
- struct lyd_node **data)
+np_op_filter_data_get(struct np_user_sess *user_sess, sr_datastore_t ds, uint32_t max_depth, uint32_t get_opts,
+ const char *xp_filter, struct lyd_node **data)
{
sr_data_t *sr_data = NULL, *sr_ln2_nc_server = NULL;
struct lyd_node *e, *ignored_mod;
@@ -1255,22 +1259,31 @@ np_op_filter_data_get(sr_session_ctx_t *session, uint32_t max_depth, uint32_t ge
return NULL;
}
- /* get the selected data */
- r = sr_get_data(session, xp_filter, max_depth, np2srv.sr_timeout, get_opts, &sr_data);
+ /* update sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, ds);
+
+ if (user_sess->use_private_cand && (ds == SR_DS_CANDIDATE)) {
+ /* use private candidate if supported and requested */
+ r = sr_pc_get_data(user_sess->sess, xp_filter, max_depth, get_opts, user_sess->private_ds, &sr_data);
+ } else {
+ /* get the selected data */
+ r = sr_get_data(user_sess->sess, xp_filter, max_depth, np2srv.sr_timeout, get_opts, &sr_data);
+ }
+
if (r && (r != SR_ERR_NOT_FOUND)) {
ERR("Getting data \"%s\" from sysrepo failed (%s).", xp_filter, sr_strerror(r));
- sr_session_get_error(session, &err_info);
+ sr_session_get_error(user_sess->sess, &err_info);
err = &err_info->err[0];
if (strstr(err->message, " result is not a node set.")) {
/* invalid-value */
- e = nc_err(sr_session_acquire_context(session), NC_ERR_INVALID_VALUE, NC_ERR_TYPE_APP);
- sr_session_release_context(session);
+ e = nc_err(sr_session_acquire_context(user_sess->sess), NC_ERR_INVALID_VALUE, NC_ERR_TYPE_APP);
+ sr_session_release_context(user_sess->sess);
nc_err_set_msg(e, err->message, "en");
reply = nc_server_reply_err(e);
} else {
/* other error */
- reply = np_reply_err_sr(session, "get");
+ reply = np_reply_err_sr(user_sess->sess, "get");
}
goto cleanup;
}
@@ -1302,7 +1315,7 @@ np_op_filter_data_get(sr_session_ctx_t *session, uint32_t max_depth, uint32_t ge
sr_release_data(sr_data);
if (r) {
/* other error */
- reply = np_reply_err_op_failed(session, NULL, ly_last_logmsg());
+ reply = np_reply_err_op_failed(user_sess->sess, NULL, ly_last_logmsg());
goto cleanup;
}
}
@@ -1881,3 +1894,139 @@ sub_ntf_ds2ident(sr_datastore_t ds)
return NULL;
}
+
+/**
+ * @brief Convert private candidate conflict type to string.
+ *
+ * @param[in] type Conflict type to convert.
+ * @return String representation of the conflict type.
+ */
+static const char *
+np_pc_conflict_type2str(sr_pc_conflict_type_t type)
+{
+ switch (type) {
+ case SR_PC_CONFLICT_VALUE_CHANGE:
+ return "value-change";
+ case SR_PC_CONFLICT_LIST_ENTRY:
+ return "list-entry";
+ case SR_PC_CONFLICT_LIST_ORDER:
+ return "list-order";
+ case SR_PC_CONFLICT_PRESENCE_CONTAINER:
+ return "presence-container";
+ case SR_PC_CONFLICT_LEAFLIST_ITEM:
+ return "leaf-list-item";
+ case SR_PC_CONFLICT_LEAFLIST_ORDER:
+ return "leaf-list-order";
+ case SR_PC_CONFLICT_LEAF_EXISTENCE:
+ return "leaf-existence";
+ }
+
+ return NULL;
+}
+
+/**
+ * @brief Get the value of the conflict node.
+ *
+ * @param[in] type Conflict node to get the value from.
+ * @return String value of node. In case of list the keys are returned.
+ */
+static char *
+np_pc_conflict_value(const struct lyd_node *node)
+{
+ char *full_path = NULL, *list_keys = NULL, *result = NULL;
+ const char *val;
+
+ switch (node->schema->nodetype) {
+ case LYS_CONTAINER:
+ /* container does not have value */
+ return NULL;
+
+ case LYS_LIST:
+ full_path = lyd_path(node, LYD_PATH_STD, NULL, 0);
+ if (full_path) {
+ list_keys = strchr(full_path, '[');
+ if (list_keys) {
+ result = strdup(list_keys);
+ }
+ free(full_path);
+ }
+ return result;
+
+ default:
+ val = lyd_get_value(node);
+ return val ? strdup(val) : NULL;
+ }
+}
+
+struct nc_server_reply *
+np_reply_err_conflict(const struct lyd_node *rpc, sr_pc_conflict_set_t *conflict_set)
+{
+ struct lyd_node *err, *err_info_node;
+ char *run_val, *cand_val, *xpath = NULL;
+ uint32_t i;
+ int ret;
+
+ err = nc_err(LYD_CTX(rpc), NC_ERR_OP_FAILED, NC_ERR_TYPE_APP);
+ nc_err_set_msg(err, "Update failed due to conflicts between private candidate and running configuration.", NULL);
+
+ if (conflict_set) {
+ for (i = 0; i < conflict_set->conflict_count; i++) {
+ err_info_node = NULL;
+
+ /* conflict node */
+ if ((ret = lyd_new_opaq2(NULL, LYD_CTX(rpc), "conflict", NULL, NULL,
+ "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate", &err_info_node))) {
+ goto internal_error;
+ }
+
+ /* xpath of the conflicting node */
+ xpath = lyd_path(conflict_set->conflicts[i].run_diff, LYD_PATH_STD, NULL, 0);
+ if ((ret = lyd_new_opaq2(err_info_node, LYD_CTX(rpc), "xpath", xpath, NULL,
+ "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate", NULL))) {
+ goto internal_error;
+ }
+
+ /* conflict type */
+ if ((ret = lyd_new_opaq2(err_info_node, LYD_CTX(rpc), "conflict-type",
+ np_pc_conflict_type2str(conflict_set->conflicts[i].type),
+ NULL, "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate", NULL))) {
+ goto internal_error;
+ }
+
+ /* values where the conflict occurs */
+ run_val = np_pc_conflict_value(conflict_set->conflicts[i].run_diff);
+ if (run_val) {
+ ret = lyd_new_opaq2(err_info_node, LYD_CTX(rpc), "value-running",
+ run_val, NULL, "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate", NULL);
+ free(run_val);
+ if (ret) {
+ goto internal_error;
+ }
+ }
+
+ cand_val = np_pc_conflict_value(conflict_set->conflicts[i].pc_diff);
+ if (cand_val) {
+ ret = lyd_new_opaq2(err_info_node, LYD_CTX(rpc), "value-candidate",
+ cand_val, NULL, "urn:ietf:params:xml:ns:yang:ietf-netconf-private-candidate", NULL);
+ free(cand_val);
+ if (ret) {
+ goto internal_error;
+ }
+
+ }
+
+ /* add conflict node into error msg*/
+ nc_err_add_info_other(err, err_info_node);
+ }
+ }
+
+ free(xpath);
+ sr_pc_free_conflicts(conflict_set);
+ return nc_server_reply_err(err);
+
+internal_error:
+ free(xpath);
+ sr_pc_free_conflicts(conflict_set);
+ lyd_free_tree(err_info_node);
+ return np_reply_err_op_failed(NULL, LYD_CTX(rpc), "Failed to build conflict error message.");
+}
diff --git a/src/common.h b/src/common.h
index d9f12c54..df7a0ba0 100644
--- a/src/common.h
+++ b/src/common.h
@@ -26,6 +26,7 @@
#include
#include
+#include
#include "compat.h"
#include "config.h"
@@ -57,6 +58,11 @@ struct np_user_sess {
pthread_mutex_t lock;
struct np_ntf_arg ntf_arg;
+
+ int use_private_cand; /* flag to use private candidate instead of shared candidate */
+ sr_priv_cand_t *private_ds; /* private candidate structure */
+ sr_priv_cand_t *private_ds_backup; /* backup of private candidate */
+ int privcand_lock;
};
/* server internal data */
@@ -79,6 +85,17 @@ struct np2srv {
extern struct np2srv np2srv;
+#define NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, label) \
+ do { \
+ if (!(user_sess)->private_ds) { \
+ /* create private candidate if not yet created */ \
+ if (sr_pc_create_ds((user_sess)->sess, 0, NULL, &(user_sess)->private_ds)) { \
+ (reply) = np_reply_err_sr((user_sess)->sess, LYD_NAME(rpc)); \
+ goto label; \
+ } \
+ } \
+ } while (0)
+
/**
* @brief Sleep in milliseconds.
*
@@ -333,15 +350,16 @@ struct nc_server_reply *np_op_parse_config(struct lyd_node_any *node, uint32_t p
/**
* @brief Get all data matching the NP2 filter.
*
- * @param[in] session SR session to get the data on.
+ * @param[in] user_sess User session to use for the get operation.
+ * @param[in] ds Datastore to get data from.
* @param[in] max_depth Max depth fo the retrieved data.
* @param[in] get_opts SR get options to use.
* @param[in] xp_filter XPath filter to use.
* @param[out] data Retrieved data.
* @return Error reply on error, NULL on success.
*/
-struct nc_server_reply *np_op_filter_data_get(sr_session_ctx_t *session, uint32_t max_depth, uint32_t get_opts,
- const char *xp_filter, struct lyd_node **data);
+struct nc_server_reply *np_op_filter_data_get(struct np_user_sess *user_sess, sr_datastore_t ds, uint32_t max_depth,
+ uint32_t get_opts, const char *xp_filter, struct lyd_node **data);
/**
* @brief Create NC data/OK reply.
@@ -450,4 +468,13 @@ struct nc_server_reply *np_reply_err_in_use(const struct ly_ctx *ly_ctx, const c
*/
const char *sub_ntf_ds2ident(sr_datastore_t ds);
+/**
+ * @brief Build a NETCONF error message for conflicts during update.
+ *
+ * @param[in] rpc Executed RPC.
+ * @param[in] conflict_set Set of conflicts.
+ * @return Server reply structure.
+ */
+struct nc_server_reply *np_reply_err_conflict(const struct lyd_node *rpc, sr_pc_conflict_set_t *conflict_set);
+
#endif /* NP2SRV_COMMON_H_ */
diff --git a/src/main.c b/src/main.c
index 3a76ed76..6f472b8a 100644
--- a/src/main.c
+++ b/src/main.c
@@ -34,6 +34,7 @@
#include
#include
#include
+#include
#include
#include "common.h"
@@ -135,8 +136,15 @@ np2srv_del_session_cb(struct nc_session *session)
/* terminate any subscriptions for the NETCONF session */
np_sub_ntf_session_destroy(session);
- /* stop sysrepo session subscriptions */
user_sess = nc_session_get_data(session);
+
+ /* destroy private candidate datastores */
+ sr_pc_destroy_ds(user_sess->sess, user_sess->private_ds);
+ user_sess->private_ds = NULL;
+ sr_pc_destroy_ds(user_sess->sess, user_sess->private_ds_backup);
+ user_sess->private_ds_backup = NULL;
+
+ /* stop sysrepo session subscriptions */
sr_session_unsubscribe(user_sess->sess);
/* revert any pending confirmed commits */
@@ -354,6 +362,11 @@ np2srv_rpc_cb(struct lyd_node *rpc, struct nc_session *ncs)
if (!strcmp(LYD_NAME(rpc), "resync-subscription")) {
rpc_cb = np2srv_rpc_resync_sub_cb;
}
+ } else if (!strcmp(lyd_owner_module(rpc)->name, "ietf-netconf-private-candidate")) {
+ /* ietf-netconf-private-candidate */
+ if (!strcmp(LYD_NAME(rpc), "update")) {
+ rpc_cb = np2srv_rpc_update_cb;
+ }
}
if (rpc_cb) {
@@ -921,6 +934,7 @@ server_init(void)
goto error;
}
#endif /* NC_ENABLED_SSH_TLS */
+ nc_server_set_capability("urn:ietf:params:netconf:capability:private-candidate:1.0");
/* set capabilities for the NETCONF Notifications */
nc_server_set_capability("urn:ietf:params:netconf:capability:notification:1.0");
diff --git a/src/netconf.c b/src/netconf.c
index dee174a2..ba2f3972 100644
--- a/src/netconf.c
+++ b/src/netconf.c
@@ -32,6 +32,7 @@
#include
#include
#include
+#include
#include
#include "common.h"
@@ -41,15 +42,15 @@
#include "netconf_monitoring.h"
/**
- * @brief Get data for a get RPC.
+ * @brief Acquire data for a get RPC.
*
- * @param[in] session User SR session.
+ * @param[in] user_sess User session.
* @param[in] xp_filter XPath filter to use.
* @param[out] data Retrieved data.
* @return Error reply on error, NULL on success.
*/
static struct nc_server_reply *
-np_get_rpc_data(sr_session_ctx_t *session, const char *xp_filter, struct lyd_node **data)
+np_get_rpc_data(struct np_user_sess *user_sess, const char *xp_filter, struct lyd_node **data)
{
struct nc_server_reply *reply = NULL;
struct lyd_node *node, *base_data = NULL;
@@ -64,19 +65,18 @@ np_get_rpc_data(sr_session_ctx_t *session, const char *xp_filter, struct lyd_nod
}
/* get base data from running */
- sr_session_switch_ds(session, SR_DS_RUNNING);
- if ((reply = np_op_filter_data_get(session, 0, SR_GET_NO_FILTER, xp_filter, &base_data))) {
+ if ((reply = np_op_filter_data_get(user_sess, SR_DS_RUNNING, 0, SR_GET_NO_FILTER, xp_filter, &base_data))) {
goto cleanup;
}
/* then append base operational data */
- sr_session_switch_ds(session, SR_DS_OPERATIONAL);
- if ((reply = np_op_filter_data_get(session, 0, SR_OPER_NO_CONFIG | SR_GET_NO_FILTER, xp_filter, &base_data))) {
+ if ((reply = np_op_filter_data_get(user_sess, SR_DS_OPERATIONAL, 0, SR_OPER_NO_CONFIG | SR_GET_NO_FILTER, xp_filter,
+ &base_data))) {
goto cleanup;
}
if (!strcmp(xp_filter, "/*") || !base_data) {
- /* no filter, use all the data, or no data at all */
+ /* no filter, use all Data, or no data at all */
*data = base_data;
base_data = NULL;
goto cleanup;
@@ -84,13 +84,13 @@ np_get_rpc_data(sr_session_ctx_t *session, const char *xp_filter, struct lyd_nod
/* now filter only the requested data from the created running data + state data */
if (lyd_find_xpath3(NULL, base_data, xp_filter, LY_VALUE_JSON, NULL, NULL, &set)) {
- reply = np_reply_err_op_failed(session, NULL, ly_last_logmsg());
+ reply = np_reply_err_op_failed(user_sess->sess, NULL, ly_last_logmsg());
goto cleanup;
}
for (i = 0; i < set->count; ++i) {
if (lyd_dup_single(set->dnodes[i], NULL, LYD_DUP_RECURSIVE | LYD_DUP_WITH_PARENTS | LYD_DUP_WITH_FLAGS, &node)) {
- reply = np_reply_err_op_failed(session, NULL, ly_last_logmsg());
+ reply = np_reply_err_op_failed(user_sess->sess, NULL, ly_last_logmsg());
goto cleanup;
}
@@ -102,7 +102,7 @@ np_get_rpc_data(sr_session_ctx_t *session, const char *xp_filter, struct lyd_nod
/* merge */
if (lyd_merge_tree(data, node, LYD_MERGE_DESTRUCT)) {
lyd_free_tree(node);
- reply = np_reply_err_op_failed(session, NULL, ly_last_logmsg());
+ reply = np_reply_err_op_failed(user_sess->sess, NULL, ly_last_logmsg());
goto cleanup;
}
}
@@ -174,18 +174,20 @@ np2srv_rpc_get_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
/* we do not care here about with-defaults mode, it does not change anything */
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+ }
+
/* get filtered data */
if (!strcmp(LYD_NAME(rpc), "get-config")) {
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
-
/* create the data tree for the data reply */
- if ((reply = np_op_filter_data_get(user_sess->sess, 0, 0, xp_filter, &data_get))) {
+ if ((reply = np_op_filter_data_get(user_sess, ds, 0, 0, xp_filter, &data_get))) {
goto cleanup;
}
} else {
/* get properly merged data */
- if ((reply = np_get_rpc_data(user_sess->sess, xp_filter, &data_get))) {
+ if ((reply = np_get_rpc_data(user_sess, xp_filter, &data_get))) {
goto cleanup;
}
}
@@ -210,6 +212,51 @@ np2srv_rpc_get_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
return reply;
}
+/**
+ * @brief Handles the edit-config operation for the Private Candidate datastore.
+ *
+ * @param[in] rpc RPC data tree.
+ * @param[in] user_sess User session.
+ * @param[in] defop Default operation for the edit-config.
+ * @param[in] testop Test option.
+ * @param[in] config Data tree containing configuration changes.
+ * @return Error reply on error, NULL on success.
+ */
+static struct nc_server_reply *
+np2srv_pc_editconfig(const struct lyd_node *rpc, struct np_user_sess *user_sess, const char *defop, const char *testop,
+ const struct lyd_node *config)
+{
+ struct nc_server_reply *reply = NULL;
+ sr_priv_cand_t *dup_privcand = NULL;
+
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ /* duplicate private candidate for test-only or test-then-set */
+ if (sr_pc_backup_privcand(user_sess->sess, user_sess->private_ds, &dup_privcand)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ if (sr_pc_edit_config(user_sess->sess, user_sess->private_ds, config, defop)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ if (sr_pc_validate(user_sess->sess, NULL, user_sess->private_ds)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ if (!strcmp(testop, "test-only")) {
+ /* validate only, revert the changes made */
+ sr_pc_restore_privcand(user_sess->private_ds, dup_privcand);
+ }
+
+cleanup:
+ return reply;
+}
+
struct nc_server_reply *
np2srv_rpc_editconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
{
@@ -275,26 +322,33 @@ np2srv_rpc_editconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_s
}
ly_set_free(nodeset, NULL);
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
-
- /* sysrepo API */
- if (config && sr_edit_batch(user_sess->sess, config, defop)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
- }
-
- if (!strcmp(testop, "test-then-set")) {
- if (sr_apply_changes(user_sess->sess, np2srv.sr_timeout)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ /* private candidate */
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ if ((reply = np2srv_pc_editconfig(rpc, user_sess, defop, testop, config))) {
goto cleanup;
}
} else {
- assert(!strcmp(testop, "test-only"));
- if (sr_validate(user_sess->sess, NULL, 0)) {
+ /* update sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, ds);
+
+ /* sysrepo API */
+ if (config && sr_edit_batch(user_sess->sess, config, defop)) {
reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
goto cleanup;
}
+
+ if (!strcmp(testop, "test-then-set")) {
+ if (sr_apply_changes(user_sess->sess, np2srv.sr_timeout)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ } else {
+ assert(!strcmp(testop, "test-only"));
+ if (sr_validate(user_sess->sess, NULL, 0)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ }
}
/* OK reply */
@@ -308,6 +362,78 @@ np2srv_rpc_editconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_s
return reply;
}
+/**
+ * @brief Handles the copy-config operation for the Private Candidate datastore.
+ *
+ * @param[in] rpc RPC data tree.
+ * @param[in] user_sess User session.
+ * @param[in] ds Target datastore.
+ * @param[in] sds Source datastore.
+ * @param[in] source_is_config Flag indicating if the source is a config data.
+ * @param[in,out] config Data tree containing configuration changes.
+ * @return Error reply on error, NULL on success.
+ */
+static struct nc_server_reply *
+np2srv_pc_copyconfig(const struct lyd_node *rpc, struct np_user_sess *user_sess, const sr_datastore_t ds,
+ const sr_datastore_t sds, const int source_is_config, struct lyd_node **config)
+{
+ struct nc_server_reply *reply = NULL;
+ sr_data_t *sr_data = NULL;
+
+ assert(user_sess->use_private_cand);
+
+ if ((ds == SR_DS_CANDIDATE)) {
+ if (source_is_config) {
+ /* config -> private candidate */
+ if (sr_pc_replace_trg_config(user_sess->sess, user_sess->private_ds, NULL, *config)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ } else {
+ /* conventional datastore -> private candidate */
+ if (sr_get_data(user_sess->sess, "/*", 0, 0, 0, &sr_data)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ if (sr_pc_replace_trg_config(user_sess->sess, user_sess->private_ds, NULL, sr_data->tree)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ }
+ } else {
+ assert(sds == SR_DS_CANDIDATE);
+
+ if (source_is_config) {
+ /* config -> conventional datastore */
+ if (sr_replace_config(user_sess->sess, NULL, *config, np2srv.sr_timeout)) {
+ *config = NULL;
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ *config = NULL;
+ } else {
+ /* private candidate -> conventional datastore */
+ if (sr_pc_get_data(user_sess->sess, "/*", 0, 0, user_sess->private_ds, &sr_data)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ /* copy from conventional datastore into candidate */
+ if (sr_replace_config(user_sess->sess, NULL, sr_data->tree, np2srv.sr_timeout)) {
+ sr_data->tree = NULL;
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ sr_data->tree = NULL;
+ }
+ }
+
+cleanup:
+ sr_release_data(sr_data);
+ return reply;
+}
+
struct nc_server_reply *
np2srv_rpc_copyconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
{
@@ -407,12 +533,23 @@ np2srv_rpc_copyconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_s
struct lyd_node *node;
if (!source_is_config) {
- /* get source datastore data */
- sr_session_switch_ds(user_sess->sess, sds);
- if (sr_get_data(user_sess->sess, "/*", 0, np2srv.sr_timeout, 0, &sr_data)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ if ((sds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ if (sr_pc_get_data(user_sess->sess, "/*", 0, 0, user_sess->private_ds, &sr_data)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ } else {
+ /* get source datastore data */
+ sr_session_switch_ds(user_sess->sess, sds);
+ if (sr_get_data(user_sess->sess, "/*", 0, np2srv.sr_timeout, 0, &sr_data)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
+
config = sr_data->tree;
sr_data->tree = NULL;
sr_release_data(sr_data);
@@ -439,30 +576,39 @@ np2srv_rpc_copyconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user_s
} else
#endif
{
- if (source_is_config) {
- /* config is spent */
- if (sr_replace_config(user_sess->sess, NULL, config, np2srv.sr_timeout)) {
- config = NULL;
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ if (((ds == SR_DS_CANDIDATE) || (sds == SR_DS_CANDIDATE)) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ if ((reply = np2srv_pc_copyconfig(rpc, user_sess, ds, sds, source_is_config, &config))) {
goto cleanup;
}
- config = NULL;
} else {
- if (run_to_start) {
- /* skip NACM check */
- sr_nacm_set_user(user_sess->sess, NULL);
- }
+ if (source_is_config) {
+ /* config is spent */
+ if (sr_replace_config(user_sess->sess, NULL, config, np2srv.sr_timeout)) {
+ config = NULL;
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ config = NULL;
+ } else {
+ if (run_to_start) {
+ /* skip NACM check */
+ sr_nacm_set_user(user_sess->sess, NULL);
+ }
- if (sr_copy_config(user_sess->sess, NULL, sds, np2srv.sr_timeout)) {
- /* prevent the error info being overwritten */
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- }
+ if (sr_copy_config(user_sess->sess, NULL, sds, np2srv.sr_timeout)) {
+ /* prevent the error info being overwritten */
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ }
- /* set NACM username back */
- sr_nacm_set_user(user_sess->sess, nc_session_get_username(user_sess->ntf_arg.nc_sess));
+ /* set NACM username back */
+ sr_nacm_set_user(user_sess->sess, nc_session_get_username(user_sess->ntf_arg.nc_sess));
- if (reply) {
- goto cleanup;
+ if (reply) {
+ goto cleanup;
+ }
}
}
}
@@ -537,6 +683,33 @@ np2srv_rpc_deleteconfig_cb(const struct lyd_node *rpc, struct np_user_sess *user
return reply;
}
+struct nc_server_reply *
+np2srv_pc_un_lock(const struct lyd_node *rpc, struct np_user_sess *user_sess, struct nc_session *nc_sess)
+{
+ struct nc_server_reply *reply = NULL;
+
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ if (!strcmp(rpc->schema->name, "lock")) {
+ if (user_sess->privcand_lock) {
+ reply = np_reply_err_lock_denied(LYD_CTX(rpc), "Lock failed, is already locked.", nc_session_get_id(nc_sess));
+ goto cleanup;
+ }
+ user_sess->privcand_lock = 1;
+ } else {
+ assert(!strcmp(rpc->schema->name, "unlock"));
+ if (!user_sess->privcand_lock) {
+ reply = np_reply_err_lock_denied(LYD_CTX(rpc), "Unlock failed, was not locked.", nc_session_get_id(nc_sess));
+ goto cleanup;
+ }
+ user_sess->privcand_lock = 0;
+ }
+
+cleanup:
+ return reply;
+}
+
struct nc_server_reply *
np2srv_rpc_un_lock_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
{
@@ -571,19 +744,27 @@ np2srv_rpc_un_lock_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess
goto cleanup;
}
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
-
/* sysrepo API */
- if (!strcmp(rpc->schema->name, "lock")) {
- r = sr_lock(user_sess->sess, NULL, NP2SRV_DS_LOCK_TIMEOUT);
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* lock/unlock for private candidate */
+ if ((reply = np2srv_pc_un_lock(rpc, user_sess, nc_sess))) {
+ goto cleanup;
+ }
} else {
- assert(!strcmp(rpc->schema->name, "unlock"));
- r = sr_unlock(user_sess->sess, NULL);
- }
- if (r) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ /* update sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, ds);
+
+ if (!strcmp(rpc->schema->name, "lock")) {
+ r = sr_lock(user_sess->sess, NULL, NP2SRV_DS_LOCK_TIMEOUT);
+ } else {
+ assert(!strcmp(rpc->schema->name, "unlock"));
+ r = sr_unlock(user_sess->sess, NULL);
+ }
+
+ if (r) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
/* OK reply */
@@ -637,13 +818,19 @@ np2srv_rpc_discard_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess
{
struct nc_server_reply *reply = NULL;
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, SR_DS_CANDIDATE);
+ if (user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+ sr_pc_discard_changes(user_sess->private_ds);
+ } else {
+ /* update sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, SR_DS_CANDIDATE);
- /* sysrepo API */
- if (sr_copy_config(user_sess->sess, NULL, SR_DS_RUNNING, np2srv.sr_timeout)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ /* sysrepo API */
+ if (sr_copy_config(user_sess->sess, NULL, SR_DS_RUNNING, np2srv.sr_timeout)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
/* OK reply */
@@ -693,14 +880,26 @@ np2srv_rpc_validate_cb(const struct lyd_node *rpc, struct np_user_sess *user_ses
ly_set_free(nodeset, NULL);
if (!config) {
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
- /* sysrepo API */
- if (sr_validate(user_sess->sess, NULL, 0)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ /* validate private candidate */
+ if (sr_pc_validate(user_sess->sess, NULL, user_sess->private_ds)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ } else {
+ /* update sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, ds);
+
+ /* sysrepo API */
+ if (sr_validate(user_sess->sess, NULL, 0)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
+
} /* else already validated */
/* OK reply */
@@ -711,6 +910,52 @@ np2srv_rpc_validate_cb(const struct lyd_node *rpc, struct np_user_sess *user_ses
return reply;
}
+struct nc_server_reply *
+np2srv_rpc_update_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
+{
+ sr_pc_conflict_resolution_t resolution_mode = SR_PC_REVERT_ON_CONFLICT;
+ sr_pc_conflict_set_t *conflict_set = NULL;
+ struct nc_server_reply *reply = NULL;
+ const char *str_resolution_mode;
+ struct lyd_node *leaf = NULL;
+ int ret;
+
+ /* get conflict resolution mode */
+ lyd_find_path(rpc, "resolution-mode", 0, &leaf);
+
+ if (leaf) {
+ str_resolution_mode = lyd_get_value(leaf);
+ if (!strcmp(str_resolution_mode, "revert-on-conflict")) {
+ resolution_mode = SR_PC_REVERT_ON_CONFLICT;
+ } else if (!strcmp(str_resolution_mode, "prefer-running")) {
+ resolution_mode = SR_PC_PREFER_RUNNING;
+ } else {
+ assert(!strcmp(str_resolution_mode, "prefer-candidate"));
+ resolution_mode = SR_PC_PREFER_CANDIDATE;
+ }
+ }
+
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ ret = sr_pc_update(user_sess->sess, user_sess->private_ds, resolution_mode, &conflict_set);
+ if (ret != SR_ERR_OK) {
+ if (ret == SR_ERR_OPERATION_FAILED) {
+ /* build error msg with conflict info */
+ reply = np_reply_err_conflict(rpc, conflict_set);
+ goto cleanup;
+ }
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ /* OK reply */
+ reply = np_reply_success(rpc, NULL);
+
+cleanup:
+ return reply;
+}
+
/**
* @brief New notification callback used for notifications received on subscription made by \ RPC.
*/
diff --git a/src/netconf.h b/src/netconf.h
index 10e29401..04324e68 100644
--- a/src/netconf.h
+++ b/src/netconf.h
@@ -42,6 +42,8 @@ struct nc_server_reply *np2srv_rpc_discard_cb(const struct lyd_node *rpc, struct
struct nc_server_reply *np2srv_rpc_validate_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess);
+struct nc_server_reply *np2srv_rpc_update_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess);
+
struct nc_server_reply *np2srv_rpc_subscribe_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess);
int np2srv_nc_ntf_oper_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *module_name, const char *path,
diff --git a/src/netconf_confirmed_commit.c b/src/netconf_confirmed_commit.c
index 04c084ec..22009fe6 100644
--- a/src/netconf_confirmed_commit.c
+++ b/src/netconf_confirmed_commit.c
@@ -36,6 +36,7 @@
#include
#include
#include
+#include
#include "common.h"
#include "compat.h"
@@ -504,6 +505,13 @@ ncc_changes_rollback_cb(union sigval sev)
goto cleanup;
}
+ /* private candidate rollback */
+ if (user_sess && user_sess->use_private_cand && user_sess->private_ds) {
+ VRB("Rolling back private candidate datastore.");
+ sr_pc_restore_privcand(user_sess->private_ds, user_sess->private_ds_backup);
+ user_sess->private_ds_backup = NULL;
+ }
+
/* just timer clean up */
ncc_commit_confirmed();
@@ -834,6 +842,7 @@ np2srv_confirmed_commit_cb(const struct lyd_node *rpc, struct np_user_sess *user
struct lyd_node *node = NULL;
uint32_t timeout;
uint8_t timeout_changed = 0;
+ sr_pc_conflict_set_t *conflict_set = NULL;
nc_sess = user_sess->ntf_arg.nc_sess;
@@ -859,6 +868,16 @@ np2srv_confirmed_commit_cb(const struct lyd_node *rpc, struct np_user_sess *user
if (ncc_running_backup(LYD_CTX(rpc))) {
goto cleanup;
}
+
+ /* private candidate create and store backup */
+ if (user_sess->use_private_cand && user_sess->private_ds) {
+ if (sr_pc_backup_privcand(user_sess->sess, user_sess->private_ds, &user_sess->private_ds_backup)) {
+ reply = np_reply_err_op_failed(user_sess->sess, LYD_CTX(rpc),
+ "Failed to create backup of private candidate datastore.");
+ goto cleanup;
+ }
+ }
+
} else {
if (commit_ctx.persist) {
if (!persist || strcmp(persist, commit_ctx.persist)) {
@@ -906,12 +925,21 @@ np2srv_confirmed_commit_cb(const struct lyd_node *rpc, struct np_user_sess *user
np_send_notif_confirmed_commit(nc_sess, user_sess->sess, NP_CC_START, timeout, 0);
}
- sr_session_switch_ds(user_sess->sess, SR_DS_RUNNING);
-
/* sysrepo API */
- if (sr_copy_config(user_sess->sess, NULL, SR_DS_CANDIDATE, np2srv.sr_timeout)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ if (user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ if (sr_pc_commit(user_sess->sess, user_sess->private_ds, &conflict_set)) {
+ reply = np_reply_err_conflict(rpc, conflict_set);
+ goto cleanup;
+ }
+ } else {
+ sr_session_switch_ds(user_sess->sess, SR_DS_RUNNING);
+ if (sr_copy_config(user_sess->sess, NULL, SR_DS_CANDIDATE, np2srv.sr_timeout)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
/* OK reply */
@@ -927,6 +955,7 @@ np2srv_rpc_commit_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
struct nc_server_reply *reply = NULL;
struct lyd_node *node;
const char *persist_id = NULL, *persist;
+ sr_pc_conflict_set_t *conflict_set = NULL;
/* LOCK */
pthread_mutex_lock(&commit_ctx.lock);
@@ -962,10 +991,28 @@ np2srv_rpc_commit_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess)
}
/* sysrepo API */
- sr_session_switch_ds(user_sess->sess, SR_DS_RUNNING);
- if (sr_copy_config(user_sess->sess, NULL, SR_DS_CANDIDATE, np2srv.sr_timeout)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ if (user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+
+ /* final commit and destroy the backup */
+ if (user_sess->private_ds_backup) {
+ if (sr_pc_destroy_ds(user_sess->sess, user_sess->private_ds_backup)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ user_sess->private_ds_backup = NULL;
+ }
+ if (sr_pc_commit(user_sess->sess, user_sess->private_ds, &conflict_set)) {
+ reply = np_reply_err_conflict(rpc, conflict_set);
+ goto cleanup;
+ }
+ } else {
+ sr_session_switch_ds(user_sess->sess, SR_DS_RUNNING);
+ if (sr_copy_config(user_sess->sess, NULL, SR_DS_CANDIDATE, np2srv.sr_timeout)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
/* OK reply */
diff --git a/src/netconf_nmda.c b/src/netconf_nmda.c
index 8064e382..53d254ea 100644
--- a/src/netconf_nmda.c
+++ b/src/netconf_nmda.c
@@ -26,6 +26,7 @@
#include
#include
+#include
#include
#include "common.h"
@@ -180,11 +181,13 @@ np2srv_rpc_getdata_cb(const struct lyd_node *rpc, struct np_user_sess *user_sess
}
}
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
+ }
/* create the data tree for the data reply */
- if ((reply = np_op_filter_data_get(user_sess->sess, max_depth, get_opts, xp_filter, &data))) {
+ if ((reply = np_op_filter_data_get(user_sess, ds, max_depth, get_opts, xp_filter, &data))) {
goto cleanup;
}
@@ -272,16 +275,31 @@ np2srv_rpc_editdata_cb(const struct lyd_node *rpc, struct np_user_sess *user_ses
#endif
}
- /* update sysrepo session datastore */
- sr_session_switch_ds(user_sess->sess, ds);
+ if ((ds == SR_DS_CANDIDATE) && user_sess->use_private_cand) {
+ /* create private candidate if not yet created */
+ NP2_CHECK_PRIVCAND_EXISTS(user_sess, rpc, reply, cleanup);
- /* sysrepo API */
- if (sr_edit_batch(user_sess->sess, config, defop)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- }
- if (sr_apply_changes(user_sess->sess, np2srv.sr_timeout)) {
- reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
- goto cleanup;
+ if (sr_pc_edit_config(user_sess->sess, user_sess->private_ds, config, defop)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+
+ if (sr_pc_validate(user_sess->sess, NULL, user_sess->private_ds)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
+ } else {
+ /* change sysrepo session datastore */
+ sr_session_switch_ds(user_sess->sess, ds);
+
+ /* sysrepo API */
+ if (sr_edit_batch(user_sess->sess, config, defop)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ }
+ if (sr_apply_changes(user_sess->sess, np2srv.sr_timeout)) {
+ reply = np_reply_err_sr(user_sess->sess, LYD_NAME(rpc));
+ goto cleanup;
+ }
}
/* OK reply */
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 0146cc39..556bfd24 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -42,7 +42,8 @@ set(TEST_SRC "${CMAKE_CURRENT_SOURCE_DIR}/np2_test.c" "${CMAKE_CURRENT_SOURCE_DI
# list of all the tests
set(TESTS test_rpc test_edit test_filter test_subscribe_filter test_subscribe_param test_parallel_sessions
- test_candidate test_with_defaults test_nacm test_sub_ntf test_sub_ntf_advanced test_sub_ntf_filter test_error test_other_client)
+ test_candidate test_with_defaults test_nacm test_sub_ntf test_sub_ntf_advanced test_sub_ntf_filter test_error test_other_client
+ test_privcand)
if(CMAKE_C_FLAGS MATCHES "-fsanitize=thread")
message(WARNING "Features which use SIGEV_THREAD are known to be broken under TSAN, disabling tests for YANG-push and confirmed commit")
diff --git a/tests/np2_test.c b/tests/np2_test.c
index 7588a420..17703696 100644
--- a/tests/np2_test.c
+++ b/tests/np2_test.c
@@ -236,6 +236,10 @@ np2_glob_test_setup_sess_ctx(struct nc_session *sess, const char **modules)
SETUP_FAIL_LOG;
return 1;
}
+ if (!ly_ctx_load_module(ctx, "ietf-netconf-private-candidate", "2026-02-03", all_features)) {
+ SETUP_FAIL_LOG;
+ return 1;
+ }
/* test module searchdir */
ly_ctx_set_searchdir(ctx, NP_TEST_MODULE_DIR);
diff --git a/tests/test_privcand.c b/tests/test_privcand.c
new file mode 100644
index 00000000..94827016
--- /dev/null
+++ b/tests/test_privcand.c
@@ -0,0 +1,948 @@
+/**
+ * @file test_privcand.c
+ * @author Juraj Budai
+ * @brief tests for private candidate configuration
+ *
+ * @copyright
+ * Copyright (c) 2026 Deutsche Telekom AG.
+ * Copyright (c) 2026 CESNET, z.s.p.o.
+ *
+ * This source code is licensed under BSD 3-Clause License (the "License").
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://opensource.org/licenses/BSD-3-Clause
+ */
+
+#define _GNU_SOURCE
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include "np2_test.h"
+#include "np2_test_config.h"
+
+#define UPDATE_RPC(RES_MODE) \
+ " \n" \
+ " "RES_MODE"\n" \
+ " \n" \
+
+#define TCC_NOTIF_XMLNS "\"urn:ietf:params:xml:ns:yang:ietf-netconf-notifications\""
+
+#define TCC_RECV_NOTIF_PARAM(nc_sess, timeout_ms, state) \
+ do { \
+ state->msgtype = nc_recv_notif(nc_sess, timeout_ms, &state->envp, &state->op); \
+ } while (state->msgtype == NC_MSG_REPLY); \
+ assert_int_equal(NC_MSG_NOTIF, state->msgtype); \
+ while (state->op->parent) state->op = lyd_parent(state->op); \
+
+#define TCC_RECV_NOTIF(state) \
+ TCC_RECV_NOTIF_PARAM(state->nc_sess, 3000, state)
+
+#define TCC_ASSERT_NOTIF_EVENT(state, event, ssid) \
+ { \
+ assert_int_equal(lyd_print_mem(&state->str, state->op, LYD_XML, 0), LY_SUCCESS); \
+ char *exp_cce = notif_cc_event(event, ssid); \
+ assert_non_null(exp_cce); \
+ assert_string_equal(exp_cce, state->str); \
+ free(exp_cce); \
+ free(state->str); \
+ state->str = NULL; \
+ }
+
+static char *
+notif_cc_event(const char *event, uint32_t ssid)
+{
+ char *msg = NULL;
+ int r;
+
+ /* Check data without 'timeout' leaf */
+ if (!strcmp("timeout", event)) {
+ r = asprintf(&msg,
+ "\n"
+ " timeout\n"
+ "\n");
+ assert_int_not_equal(r, -1);
+ } else {
+ r = asprintf(&msg,
+ "\n"
+ " %s\n"
+ " %" PRIu32 "\n"
+ " %s\n"
+ "\n",
+ np2_get_user(), ssid, event);
+ assert_int_not_equal(r, -1);
+ }
+
+ return msg;
+}
+
+static int
+notif_check_cc_timeout(struct np2_test *st, uint32_t expected_timeout)
+{
+ struct lyd_node *timeout_node;
+ const uint32_t timeout_tolerance = 2;
+ uint32_t timeout;
+
+ timeout_node = lyd_child(st->op)->prev;
+ if (!timeout_node || strcmp(timeout_node->schema->name, "timeout")) {
+ /* timeout node is missing */
+ return 2;
+ }
+ timeout = ((struct lyd_node_term *)timeout_node)->value.uint32;
+
+ if ((expected_timeout <= timeout_tolerance) ||
+ ((timeout >= (expected_timeout - timeout_tolerance)) &&
+ (timeout <= expected_timeout))) {
+ /* success, timeout node is checked so it can be removed */
+ lyd_free_tree(timeout_node);
+ return 0;
+ } else {
+ /* timeout is out of range */
+ return 1;
+ }
+}
+
+static int
+local_setup(void **state)
+{
+ struct np2_test *st = *state;
+ const char *modules[] = {NP_TEST_MODULE_DIR "/edit1.yang", NULL};
+ char test_name[256];
+ int rc;
+
+ /* use private candidate */
+ rc = nc_client_set_capability("urn:ietf:params:netconf:capability:private-candidate:1.0");
+ assert_int_equal(rc, 0);
+
+ /* get test name */
+ np2_glob_test_setup_test_name(test_name);
+
+ /* setup environment */
+ rc = np2_glob_test_setup_env(test_name);
+ assert_int_equal(rc, 0);
+
+ /* setup netopeer2 server */
+ rc = np2_glob_test_setup_server(state, test_name, modules, NULL, 0);
+ assert_int_equal(rc, 0);
+ st = *state;
+
+ assert_int_equal(sr_session_start(st->conn, SR_DS_CANDIDATE, &st->sr_sess2), SR_ERR_OK);
+
+ /* setup NACM */
+ rc = np2_glob_test_setup_nacm(state);
+ assert_int_equal(rc, 0);
+
+ /* Enable replay support for ietf-netconf-notifications */
+ assert_int_equal(SR_ERR_OK, sr_set_module_replay_support(st->conn, "ietf-netconf-notifications", 1));
+
+ /* Subscribe confirm-commit notification */
+ SEND_RPC_ESTABSUB(st, "/ietf-netconf-notifications:netconf-confirmed-commit",
+ "ietf-netconf-notifications", NULL, NULL);
+ ASSERT_OK_SUB_NTF(st);
+ FREE_TEST_VARS(st);
+
+ return 0;
+}
+
+static int
+local_teardown(void **state)
+{
+ struct np2_test *st = *state;
+ const char *modules[] = {"edit1", NULL};
+
+ if (!st) {
+ return 0;
+ }
+
+ free(st->path);
+
+ /* close the candidate session */
+ assert_int_equal(sr_session_stop(st->sr_sess2), SR_ERR_OK);
+
+ return np2_glob_test_teardown(state, modules);
+}
+
+static int
+setup_common(void **state)
+{
+ struct np2_test *st = *state;
+ const char *data = "Test";
+
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_CANDIDATE, data);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ return 0;
+}
+
+static int
+teardown_common(void **state)
+{
+ struct np2_test *st = *state;
+ const char *rpc = UPDATE_RPC("prefer-candidate");
+ const char *data =
+ ""
+ "";
+
+ SR_EDIT_SESSION(st, st->sr_sess, data);
+ FREE_TEST_VARS(st);
+ SR_EDIT_SESSION(st, st->sr_sess2, data);
+ FREE_TEST_VARS(st);
+
+ // temporary fix for by update and discard changes
+
+ st->rpc = nc_rpc_discard();
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 2000, &st->msgid);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ st->rpc = nc_rpc_act_generic_xml(rpc, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ st->msgtype = nc_recv_reply(st->nc_sess, st->rpc, st->msgid, 3000, &st->envp, &st->op);
+ FREE_TEST_VARS(st);
+
+ return 0;
+}
+
+static void
+test_pc_copy_config(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected_client1;
+
+ /* target: Candidate, source: Running */
+ st->rpc = nc_rpc_copy(NC_DATASTORE_CANDIDATE, NULL, NC_DATASTORE_RUNNING, NULL, NC_WD_ALL, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ GET_CONFIG_DS_FILTER(st, NC_DATASTORE_CANDIDATE, "/edit1:*");
+ expected_client1 =
+ "\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected_client1);
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_update_rpc(void **state)
+{
+ struct np2_test *st = *state;
+ const char *data1, *data2, *data_cand;
+
+ data1 = "running1";
+ data2 = "running2";
+ data_cand = "candidate1";
+
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_RUNNING, data1);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_CANDIDATE, data_cand);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_RUNNING, data2);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ const char *rpc = UPDATE_RPC("revert-on-conflict");
+
+ st->rpc = nc_rpc_act_generic_xml(rpc, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ st->msgtype = nc_recv_reply(st->nc_sess, st->rpc, st->msgid, 3000, &st->envp, &st->op);
+ assert_int_equal(st->msgtype, NC_MSG_REPLY);
+ assert_non_null(st->envp);
+
+ lyd_print_mem(&st->str, st->envp, LYD_XML, 129);
+ assert_non_null(strstr(st->str, ""));
+
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_nmda_rpcs(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected_client1;
+ const char *data_merge = "Test";
+
+ st->rpc = nc_rpc_editdata("ietf-datastores:candidate", NC_RPC_EDIT_DFLTOP_MERGE, data_merge, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ st->rpc = nc_rpc_getdata("ietf-datastores:candidate", "/edit1:*", NULL, NULL,
+ 0, 0, 0, 0, NC_WD_ALL, NC_PARAMTYPE_CONST);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ st->msgtype = nc_recv_reply(st->nc_sess, st->rpc, st->msgid, 3000, &st->envp, &st->op);
+ assert_int_equal(st->msgtype, NC_MSG_REPLY);
+ assert_non_null(st->op);
+
+ lyd_print_mem(&st->str, st->op, LYD_XML, 129);
+ expected_client1 =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected_client1);
+
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_validate(void **state)
+{
+ struct np2_test *st = *state;
+
+ st->rpc = nc_rpc_validate(NC_DATASTORE_CANDIDATE, NULL, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+}
+
+// not supported for the candidate datastore yet – test not applicable.
+
+static void
+test_pc_lock_unlock(void **state)
+{
+ struct np2_test *st = *state;
+
+ st->rpc = nc_rpc_lock(NC_DATASTORE_CANDIDATE);
+ assert_non_null(st->rpc);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ st->rpc = nc_rpc_unlock(NC_DATASTORE_CANDIDATE);
+ assert_non_null(st->rpc);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+}
+
+/* COMMIT & CONFIRMED COMMIT */
+
+static void
+test_pc_basic_commit(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected;
+
+ /* Prior to the test running of edit1 should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* Send a confirmed-commit rpc */
+ st->rpc = nc_rpc_commit(1, 0, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 600), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should now be same as candidate, same as basic commit */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* Send a commit rpc to confirm it */
+ st->rpc = nc_rpc_commit(0, 0, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'complete' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "complete", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_commit_timeout_runout(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected;
+
+ /* Prior to the test running of edit1 should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* running lock RPC */
+ st->rpc = nc_rpc_lock(NC_DATASTORE_RUNNING);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Send a confirmed-commit rpc with 1s timeout */
+ st->rpc = nc_rpc_commit(1, 1, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification with 1s timeout */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 1), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should now be same as candidate, same as basic commit */
+ GET_FILTER(st, "/edit1:first");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* There could be a potential data-race if a second passes between receiving the reply and the get-config */
+
+ /* wait for the duration of the timeout */
+ sleep(2);
+
+ /* Expect 'timeout' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "timeout", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should have reverted back to it's original value */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* running unlock RPC */
+ st->rpc = nc_rpc_unlock(NC_DATASTORE_RUNNING);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* No notification should occur */
+ ASSERT_NO_NOTIF(st);
+}
+
+static void
+test_pc_timeout_confirm(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected;
+
+ /* Prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* Send a confirmed-commit rpc with 1s timeout */
+ st->rpc = nc_rpc_commit(1, 1, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification with 1s timeout*/
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 1), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* Send a commit rpc to confirm it */
+ st->rpc = nc_rpc_commit(0, 0, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'complete' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "complete", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ sleep(2);
+
+ /* Data should remain unchanged */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* No notification should occur */
+ ASSERT_NO_NOTIF(st);
+}
+
+static void
+test_pc_timeout_confirm_modify(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected;
+ const char *data;
+
+ /* Prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* Send a confirmed-commit rpc with 1s timeout */
+ st->rpc = nc_rpc_commit(1, 1, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Send a confirmed-commit rpc with 1s timeout */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 1), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* Modify candidate to see if confirm-commit only cancels the timer */
+ data = "Alt";
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_CANDIDATE, data);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Send a commit rpc to confirm it */
+ st->rpc = nc_rpc_commit(0, 0, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ sleep(2);
+
+ /* Expect 'complete' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "complete", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Data should change */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Alt\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_cancel_commit(void **state)
+{
+ struct np2_test *st = *state;
+ const char *run_expected, *cand_expected, *data, *data1;
+ const char *rpc = UPDATE_RPC("prefer-running");
+
+ /* prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* edit running */
+ data = "val5";
+ SR_EDIT_SESSION(st, st->sr_sess, data);
+ FREE_TEST_VARS(st);
+
+ // update candidate so it has same data as running
+ st->rpc = nc_rpc_act_generic_xml(rpc, NC_PARAMTYPE_CONST);
+ assert_non_null(st->rpc);
+
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ st->msgtype = nc_recv_reply(st->nc_sess, st->rpc, st->msgid, 3000, &st->envp, &st->op);
+ FREE_TEST_VARS(st);
+
+ /* send cancel-commit rpc, should fail as there is no commit */
+ st->rpc = nc_rpc_cancel(NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* check if received an error reply */
+ ASSERT_ERROR_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* No notification should occur */
+ ASSERT_NO_NOTIF(st);
+
+ data1 = "Test";
+
+ SEND_EDIT_RPC_DS(st, NC_DATASTORE_CANDIDATE, data1);
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* send a confirmed-commit rpc with 10m timeout */
+ st->rpc = nc_rpc_commit(1, 0, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification with 10m timeout */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 600), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ run_expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ " \n"
+ " 5\n"
+ " \n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, run_expected);
+ FREE_TEST_VARS(st);
+
+ /* send cancel-commit rpc */
+ st->rpc = nc_rpc_cancel(NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'cancel' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "cancel", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* running should now be back how it was */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ run_expected =
+ "\n"
+ " \n"
+ " val\n"
+ " \n"
+ " \n"
+ " 5\n"
+ " \n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, run_expected);
+ FREE_TEST_VARS(st);
+
+ GET_CONFIG_DS_FILTER(st, NC_DATASTORE_CANDIDATE, "/edit1:*");
+ cand_expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ " \n"
+ " 5\n"
+ " \n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, cand_expected);
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_rollback_disconnect(void **state)
+{
+ struct np2_test *st = *state;
+ struct nc_session *ncs;
+ const char *expected;
+ uint32_t sid;
+ const char *data1 = "Test";
+
+ /* prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* create a new session */
+ ncs = nc_connect_unix(st->socket_path, (struct ly_ctx *)nc_session_get_ctx(st->nc_sess2));
+ assert_non_null(ncs);
+
+ st->rpc = nc_rpc_edit(NC_DATASTORE_CANDIDATE, NC_RPC_EDIT_DFLTOP_MERGE, NC_RPC_EDIT_TESTOPT_SET, NC_RPC_EDIT_ERROPT_ROLLBACK,
+ data1, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(ncs, st->rpc, 1000, &st->msgid);
+ assert_int_equal(NC_MSG_RPC, st->msgtype);
+ ASSERT_OK_REPLY_SESS(st, ncs);
+ FREE_TEST_VARS(st);
+
+ /* send a confirmed-commit rpc with 60s timeout */
+ st->rpc = nc_rpc_commit(1, 60, NULL, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(ncs, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* expect OK */
+ ASSERT_OK_REPLY_SESS(st, ncs);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification with 60s timeout */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 60), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(ncs));
+ FREE_TEST_VARS(st);
+
+ /* running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* disconnect session, commit is rolled back */
+ sid = nc_session_get_id(ncs);
+ nc_session_free(ncs, NULL);
+
+ /* reply is sent before the server callback is called so give it a chance to perform the rollback */
+ usleep(100000);
+
+ /* Expect 'cancel' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "cancel", sid);
+ FREE_TEST_VARS(st);
+
+ /* data should remain unchanged, empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+}
+
+static void
+test_cp_confirm_persist(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected, *persist = "test-persist-1";
+
+ /* Prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* Send a confirmed-commit rpc with persist */
+ st->rpc = nc_rpc_commit(1, 0, persist, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 600), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* Running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* Send commit rpc on a different session with persist-id */
+ st->rpc = nc_rpc_commit(0, 0, NULL, persist, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess2, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* Check if received an OK reply */
+ ASSERT_OK_REPLY_SESS2(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'complete' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "complete", nc_session_get_id(st->nc_sess2));
+ FREE_TEST_VARS(st);
+
+ /* Data should remain unchanged */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+}
+
+static void
+test_pc_cancel_persist(void **state)
+{
+ struct np2_test *st = *state;
+ const char *expected, *persist = "test-persist-2";
+ struct nc_session *nc_sess;
+ const char *data1 = "Test";
+
+ /* prior to the test running should be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+
+ /* start a new NC session */
+ nc_sess = nc_connect_unix(st->socket_path, (struct ly_ctx *)nc_session_get_ctx(st->nc_sess2));
+ assert_non_null(nc_sess);
+
+ st->rpc = nc_rpc_edit(NC_DATASTORE_CANDIDATE, NC_RPC_EDIT_DFLTOP_MERGE, NC_RPC_EDIT_TESTOPT_SET, NC_RPC_EDIT_ERROPT_ROLLBACK,
+ data1, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(NC_MSG_RPC, st->msgtype);
+ ASSERT_OK_REPLY_SESS(st, nc_sess);
+ FREE_TEST_VARS(st);
+
+ /* send a confirmed-commit rpc with persist */
+ st->rpc = nc_rpc_commit(1, 0, persist, NULL, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* check if received an OK reply */
+ ASSERT_OK_REPLY_PARAM(nc_sess, 3000, st)
+ FREE_TEST_VARS(st);
+
+ /* Expect 'start' notification */
+ TCC_RECV_NOTIF(st);
+ assert_int_equal(notif_check_cc_timeout(st, 600), 0);
+ TCC_ASSERT_NOTIF_EVENT(st, "start", nc_session_get_id(nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* running should now be same as candidate */
+ GET_CONFIG_FILTER(st, "/edit1:*");
+ expected =
+ "\n"
+ " \n"
+ " Test\n"
+ " \n"
+ "\n";
+ assert_string_equal(st->str, expected);
+ FREE_TEST_VARS(st);
+
+ /* disconnect NC session */
+ nc_session_free(nc_sess, NULL);
+
+ /* No notification should occur */
+ ASSERT_NO_NOTIF(st);
+
+ /* send cancel-commit rpc on a different session */
+ st->rpc = nc_rpc_cancel(persist, NC_PARAMTYPE_CONST);
+ st->msgtype = nc_send_rpc(st->nc_sess, st->rpc, 1000, &st->msgid);
+ assert_int_equal(st->msgtype, NC_MSG_RPC);
+
+ /* check if received an OK reply */
+ ASSERT_OK_REPLY(st);
+ FREE_TEST_VARS(st);
+
+ /* Expect 'cancel' notification */
+ TCC_RECV_NOTIF(st);
+ TCC_ASSERT_NOTIF_EVENT(st, "cancel", nc_session_get_id(st->nc_sess));
+ FREE_TEST_VARS(st);
+
+ /* running should now be empty */
+ ASSERT_EMPTY_CONFIG_FILTER(st, "/edit1:*");
+}
+
+int
+main(int argc, char **argv)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test_setup_teardown(test_pc_copy_config, setup_common, teardown_common),
+ cmocka_unit_test_teardown(test_pc_update_rpc, teardown_common),
+
+ cmocka_unit_test_setup_teardown(test_pc_validate, setup_common, teardown_common),
+ cmocka_unit_test_teardown(test_pc_nmda_rpcs, teardown_common),
+ cmocka_unit_test(test_pc_lock_unlock),
+
+ cmocka_unit_test_setup_teardown(test_pc_basic_commit, setup_common, teardown_common),
+ cmocka_unit_test_setup_teardown(test_pc_commit_timeout_runout, setup_common, teardown_common),
+ cmocka_unit_test_setup_teardown(test_pc_timeout_confirm, setup_common, teardown_common),
+ cmocka_unit_test_setup_teardown(test_pc_timeout_confirm_modify, setup_common, teardown_common),
+
+ cmocka_unit_test_teardown(test_pc_cancel_commit, teardown_common),
+ cmocka_unit_test_teardown(test_pc_rollback_disconnect, teardown_common),
+
+ cmocka_unit_test_setup_teardown(test_cp_confirm_persist, setup_common, teardown_common),
+ cmocka_unit_test_teardown(test_pc_cancel_persist, teardown_common),
+ };
+
+ nc_verbosity(NC_VERB_WARNING);
+ parse_arg(argc, argv);
+ return cmocka_run_group_tests(tests, local_setup, local_teardown);
+}