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); +}