Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions modules/ietf-netconf-private-candidate@2026-02-03.yang
Original file line number Diff line number Diff line change
@@ -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: <http://tools.ietf.org/wg/netconf/>
WG List: <netconf@ietf.org>

Editor: James Cumming
<james.cumming@nokia.com>

Editor: Robert Wills
<rowills@cisco.com>";
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.";
}
}
}
}
5 changes: 5 additions & 0 deletions modules/netopeer-notifications@2026-01-05.yang
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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=(
Expand Down
167 changes: 158 additions & 9 deletions src/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

#include <libyang/libyang.h>
#include <libyang/plugins_types.h>
#include <nc_client.h>
#include <nc_server.h>
#include <sysrepo/error_format.h>
#include <sysrepo/netconf_acm.h>
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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;
}
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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.");
}
Loading