RPC Operation Interface

All RPC operations are data-driven within the server, using the YANG rpc statement for the operation and SIL callback functions.

Any new protocol operation can be added by defining a new YANG rpc statement in a module, and providing the proper SIL code.

RPC Callbacks

RPC Callback Template

The 'agt_rpc_method_t' typedef in agt/agt_rpc.h is used as the callback template for all RPC callback phases.

typedef status_t (*agt_rpc_method_t)(ses_cb_t *scb, rpc_msg_t *msg, xml_node_t *methnode)

Template for RPC server callbacks.

The same template is used for all RPC callback phases The callback is expected to validate if needed and then invoke if needed.

Param scb:

session invoking the RPC

Param msg:

message in progress for this <rpc> request

the msg->rpc_input value node contains the input (if any). It is a container matching the rpc/input node for the YANG rpc

Param methnode:

XML node for the operation, which can be used in error reporting (or ignored)

Return:

return status for the phase

  • An error in validate phase will cancel invoke phase

  • An rpc-error will be added if an error is returned and the msg error Q is empty

RPC Callback Initialization

The 'agt_rpc_register_method' function in agt/agt_rpc.h is used to provide a callback function for a specific callback phase. The same function can be used for multiple phases if desired.

This function call is usually generated automatically in the SIL or SIL-SA code stubs.

status_t agt_rpc_register_method(const xmlChar *module, const xmlChar *method_name, agt_rpc_phase_t phase, agt_rpc_method_t method)

add callback for 1 phase of RPC processing

Parameters:
  • module -- name of the module that contains the RPC statement

  • method_name -- Identifier for the rpc statement

  • phase -- RPC server callback phase for this callback

    • AGT_PH_VALIDATE(0): validate phase

    • AGT_PH_INVOKE(1): invoke phase

    • AGT_PH_POST_REPLY(2): post-reply phase

  • method -- pointer to callback function to register

Returns:

status of the operation

Example Registration Code:

res = agt_rpc_register_method(
    y_toaster_M_toaster,
    y_toaster_N_make_toast,
    AGT_RPC_PH_VALIDATE,
    y_toaster_make_toast_validate);
if (res != NO_ERR) {
    return res;
}

RPC Callback Cleanup

The 'agt_rpc_unregister_method' function in agt/agt_rpc.h is used to remove a callback function for all callback phases.

Warning

This function must be called once if any callback phase was registered. Memory may not be properly freed if an RPC callback is not properly unregistered.

This function call is usually generated automatically in the SIL or SIL-SA code stubs.

void agt_rpc_unregister_method(const xmlChar *module, const xmlChar *method_name)

remove the callback functions for all phases of RPC or Action processing for the specified RPC method or action

Parameters:
  • module -- module name of RPC method or action name

  • method_name -- RPC method or action name

Example Unregistration Code:

agt_rpc_unregister_method(
    y_toaster_M_toaster,
    y_toaster_N_make_toast);

Force an RPC Operation to be Unsupported

It is possible for the SIL instrumentation to turn support for an RPC operation off in the server.

If this API is used then the RPC operation will be removed from the list of operations that can be used in the server.

void agt_rpc_unsupport_method(const xmlChar *module, const xmlChar *method_name)

mark an RPC method or action as unsupported within the server

this is needed for operations dependent on capabilities

Parameters:
  • module -- module name of RPC method (really module name)

  • method_name -- RPC method name

Use this function with care. It may break the server if this API is used on a server-provided RPC operation.

Example Usage:

agt_rpc_unsupport_method(
    y_toaster_M_toaster,
    y_toaster_N_make_toast);

RPC Message Header

The NETCONF server will parse the incoming XML message and construct an RPC message header, which is used to maintain state and any other message-specific data during the processing of an incoming <rpc> request.

rpc_msg_t

The 'rpc_msg_t' data structure defined in ncx/rpc.h is used to represent an RPC operation. It is a high-level control block which contains a low-level 'xml_msg_hdr_t' struct within it.

struct rpc_msg_t

NETCONF Server and Client RPC Request/Reply Message Header.

Public Members

dlq_hdr_t qhdr

Queue header to store RPC messages in a queue (within the session header)

xml_msg_hdr_t mhdr

generic XML message header Most in-message state is kept in the mhdr There are several places in the code where the mhdr is used alone so there is no coupling to the RPC layer.

Contains XML message prefix map and other data used to parse the request and generate the reply.

xml_attrs_t *rpc_in_attrs

incoming: top-level rpc element data Queue of xml_attr_t representing any XML attributes that were present in the <rpc> element.

A callback function may add xml_attr_t structs to this queue to send in the reply.

struct obj_template_t_ *rpc_method

incoming: Back-pointer to the object template for this RPC operation.

2nd-level method name element data, used in agt_output_filter to check get or get-config; cannot import obj.h here! The object template type will be OBJ_TYP_RPC for an RPC method For an action this will be the top-level <action> RPC wrapper in the YANG namespace

int rpc_agt_state

incoming: SERVER RPC processing state Enum value (0, 1, 2) for the current RPC callback phase.

op_errop_t rpc_err_option

Enum value for the <error-option> parameter.

This is only set if an edit operation is in progress.

op_editop_t rpc_top_editop

Enum value for the <default-operation> parameter.

This is only set if an edit operation is in progress.

val_value_t *rpc_input

Value tree representing the container of 'input' parameters for this RPC operation.

For an action, the rpc_input node is not used. Instead the rpc_actionval backptr is used instead. This is malloced in rpc_new_msg but not filled in.

struct sil_sa_cb_t_ *rpc_sil_sa_cb

backptr to SIL-SA edit control block if WITH_YCONTROL=1

dlq_hdr_t rpc_inputQ

the rpc_inputQ is used with JSON encoded input since an array is allowed at the top-level; it is used instead of rpc_input if encoding == JSON, even if only 1 array node is parsed;

Q of val_value_t *

void *rpc_user1

Void pointer that can be used by method routines to save context or whatever to store SIL-specific data.

This pointer is preserved, transferred from validate to invoke so data does not need to be regenerated or retrieved

void *rpc_user2

Same use as rpc_user1.

uint32 rpc_returncode

Internal return code used to control nested callbacks.

rpc_data_t rpc_data_type

incoming: get method reply handling builtin For RPC operations that return data, this enumeration MUST be set to indicate which type of data is returned.

  • RPC_DATA_STD: A <data> container will be used to encapsulate any returned data, within the <rpc-reply> element.

  • RPC_DATA_YANG: The <rpc-reply> element will be the only container encapsulated any returned data. If the rpc_datacb is non-NULL then it will be used as a callback to generate the rpc-reply inline, instead of buffering the output. The rpc_data and rpc_filter parameters are optionally used by the rpc_datacb function to generate a reply.

void *rpc_datacb

For operations that return streamed data, this pointer is set to the desired callback function to use for generated the data portion of the <rpc-reply> XML response.

The template for this callback is agt_rpc_data_cb_t, found in agt_rpc.h

dlq_hdr_t rpc_dataQ

For operations that return stored data, this queue of val_value_t structures can be used to provide the response data.

Each val_value_t structure will be encoded as one of the corresponding RPC output parameters. The data will be returned in order. The val_value_t nodes will be freed when the rpc_msg_t is freed

op_filter_t rpc_filter

Internal structure for optimizing subtree and XPath retrieval operations.

backptrs for get* methods.

struct agt_cfg_transaction_t_ *rpc_txcb

incoming: agent database edit transaction control block must be freed by an upper layer if set to malloced data

boolean rpc_parse_errors

load-config parse-error and &#8212;startup-error=continue flag if the val_purge_errors_from_root function is needed

xmlChar *rpc_message_id

debugging and audit message string.

Contains the message-id attribute found in the <rpc> header. backptr into rpc_in_attrs.

xmlChar *rpc_trace_id

debugging and audit message string.

Contains the trace-id attribute found in the <rpc> header. backptr into rpc_in_attrs.

boolean rpc_replay_config

TRUE if this RPC is being called in replay config mode.

boolean rpc_with_template

&#8212;with-template parameter was seen in the validate phase

dlq_hdr_t hook_inputQ

points to add_edit_value node comming from the users freed in the end of transaction.

Used only with set-hook. Contains Q of val_value_t.

boolean rpc_defer_reply

YPSERVER mode is skipping the regular rpc-reply phase and will send the reply after doing the remote task.

xml_attrs_t rpc_defer_in_attrs

the top->attrs gets deleted so a deferred rpc-reply needs to save the rpc_in_attrs.

This is malloced and cleaned when rpc-msg_t is freed

rpc_rpytyp_t rpc_reply_type

saved reply type needed for audit record

status_t rpc_status

saved processing status for audit record

time_t rpc_start_time

saved timestamp when started for audit record

const xmlChar *subrpc_filespec

saved by agt_db_api.c so the ycontrol callback function can generate an external value with the DB-API subrpc response message

RPC Validate Callback Function

../_images/rpc_validate_phase.png

The RPC validate callback function is optional to use. Its purpose is to validate any aspects of an RPC operation, beyond the constraints checked by the server engine. Only 1 validate function can register for each YANG rpc statement. The standard NETCONF operations are reserved by the server engine. There is usually zero or one of these callback functions for every 'rpc' statement in the YANG module associated with the SIL code.

It is enabled with the agt_rpc_register_method function, within the phase 1 initialization callback function.

The yangdump-sdk code generator will create this SIL callback function by default. There will be C comments in the code to indicate where your additional C code should be added.

The val_find_child (or val_child_find) function is commonly used to find particular parameters within the RPC input section, which is encoded as a val_value_t tree.

The agt_record_error function is commonly used to record any parameter or other errors. In the libtoaster example, there are internal state variables ("toaster_enabled" and "toaster_toasting"), maintained by the SIL code, which are checked in addition to any provided parameters.

Example RPC YANG definition:

    rpc make-toast {
        description
          "Make some toast.
           The toastDone notification will be sent when
           the toast is finished.
           An 'in-use' error will be returned if toast
           is already being made.
           A 'resource-denied' error will be returned
           if the toaster service is disabled.";
        input {
            leaf toasterDoneness {
                type uint32 {
                    range "1 .. 10";
                }
                default 5;
                description
                  "This variable controls how well-done is the
                   ensuing toast. It should be on a scale of 1 to 10.
                   Toast made at 10 generally is considered unfit
                   for human consumption; toast made at 1 is warmed
                   lightly.";
            }
            leaf toasterToastType {
                type identityref {
                    base toast:toast-type;
                }
                default toast:wheat-bread;
                description
                  "This variable informs the toaster of the type of
                   material that is being toasted. The toaster
                   uses this information, combined with
                   toasterDoneness, to compute for how
                   long the material must be toasted to achieve
                   the required doneness.";
            }
        }
    }

Example SIL Function:


/********************************************************************
* FUNCTION y_toaster_make_toast_validate
*
* RPC validation phase
* All YANG constraints have passed at this point.
* Add description-stmt checks in this function.
*
* INPUTS:
*     see agt/agt_rpc.h for details
*
* RETURNS:
*     error status
********************************************************************/
static status_t
    y_toaster_make_toast_validate (
        ses_cb_t *scb,
        rpc_msg_t *msg,
        xml_node_t *methnode)
{
    status_t res;
    val_value_t *errorval;
    const xmlChar *errorstr;
    val_value_t *toasterDoneness_val;
    val_value_t *toasterToastType_val;
    uint32 toasterDoneness;
    val_idref_t *toasterToastType;

    res = NO_ERR;
    errorval = NULL;
    errorstr = NULL;

    toasterDoneness_val = val_find_child(
        msg->rpc_input,
        y_toaster_M_toaster,
        y_toaster_N_toasterDoneness);
    if (toasterDoneness_val != NULL && toasterDoneness_val->res == NO_ERR) {
        toasterDoneness = VAL_UINT(toasterDoneness_val);
    }

    toasterToastType_val = val_find_child(
        msg->rpc_input,
        y_toaster_M_toaster,
        y_toaster_N_toasterToastType);
    if (toasterToastType_val != NULL && toasterToastType_val->res == NO_ERR) {
        toasterToastType = VAL_IDREF(toasterToastType_val);
    }



    /* added code starts here */
    if (toaster_enabled) {
        /* toaster service enabled, check if in use */


        if (toaster_toasting) {
            res = ERR_NCX_IN_USE;
        } else {
            /* this is where a check on bread inventory would go */

            /* this is where a check on toaster HW ready would go */
        }
    } else {
        /* toaster service disabled */
        res = ERR_NCX_RESOURCE_DENIED;
    }
    /* added code ends here */

    /* if error: set the res, errorstr, and errorval parms */
    if (res != NO_ERR) {
        agt_record_error(
            scb,
            &msg->mhdr,
            NCX_LAYER_OPERATION,
            res,
            methnode,
            NCX_NT_STRING,
            errorstr,
            NCX_NT_VAL,
            errorval);
    }

    return res;

} /* y_toaster_make_toast_validate */

RPC Invoke Callback Function

../_images/rpc_invoke_phase.png

The RPC invoke callback function is used to perform the operation requested by the client session. Only 1 invoke function can register for each YANG rpc statement. The standard NETCONF operations are reserved by the server engine. There is usually one of these callback functions for every 'rpc' statement in the YANG module associated with the SIL code.

The RPC invoke callback function is optional to use, although if no invoke callback is provided, then the operation will have no affect. Normally, this is only the case if the module is be tested by an application developer, using netconfd-pro as a server simulator.

It is enabled with the agt_rpc_register_method function, within the phase 1 initialization callback function.

The yangdump-sdk code generator will create this SIL callback function by default. There will be C comments in the code to indicate where your additional C code should be added.

For RPC operations that return either an <ok> or <rpc-error> response, there is nothing more required of the RPC invoke callback function.

For operations which return some data or <rpc-error>, the SIL code must do 1 of 2 additional tasks:

  • add a val_value_t structure to the 'rpc_dataQ' queue in the rpc_msg_t for each parameter listed in the YANG rpc 'output' section.

  • set the 'rpc_datacb' pointer in the rpc_msg_t structure to the address of your data reply callback function.

Example RPC Invoke SIL Function Registration

res = agt_rpc_register_method(
    y_toaster_M_toaster,
    y_toaster_N_make_toast,
    AGT_RPC_PH_INVOKE,
    y_toaster_make_toast_invoke);
if (res != NO_ERR) {
    return res;
}

Example RPC Invoke SIL Function


/********************************************************************
* FUNCTION y_toaster_make_toast_invoke
*
* RPC invocation phase
* All constraints have passed at this point.
* Call device instrumentation code in this function.
*
* INPUTS:
*     see agt/agt_rpc.h for details
*
* RETURNS:
*     error status
********************************************************************/
static status_t
    y_toaster_make_toast_invoke (
        ses_cb_t *scb,
        rpc_msg_t *msg,
        xml_node_t *methnode)
{
    status_t res;
    val_value_t *toasterDoneness_val;
    val_value_t *toasterToastType_val;
    uint32 toasterDoneness;
    val_idref_t *toasterToastType;

    res = NO_ERR;
    toasterDoneness = 0;

    toasterDoneness_val = val_find_child(
        msg->rpc_input,
        y_toaster_M_toaster,
        y_toaster_N_toasterDoneness);
    if (toasterDoneness_val != NULL && toasterDoneness_val->res == NO_ERR) {
        toasterDoneness = VAL_UINT(toasterDoneness_val);
    }

    toasterToastType_val = val_find_child(
        msg->rpc_input,
        y_toaster_M_toaster,
        y_toaster_N_toasterToastType);
    if (toasterToastType_val != NULL && toasterToastType_val->res == NO_ERR) {
        toasterToastType = VAL_IDREF(toasterToastType_val);
    }

    /* invoke your device instrumentation code here */

    /* make sure the toasterDoneness value is set */
    if (toasterDoneness_val == NULL) {
        toasterDoneness = 5;   /* set the default */
    }


    /* arbitrary formula to convert toaster doneness to the
     * number of seconds the toaster should be on
     */
    toaster_duration = toasterDoneness * 12;

    /* this is where the code would go to adjust the duration
     * based on the bread type
     */

    if (LOGDEBUG) {
        log_debug("\ntoaster: starting toaster for %u seconds",
                  toaster_duration);
    }

    /* this is where the code would go to start the toaster
     * heater element
     */

    /* start a timer to toast for the specified time interval */
    res = agt_timer_create(toaster_duration,
                           FALSE,
                           toaster_timer_fn,
                           NULL,
                           &toaster_timer_id);
    if (res == NO_ERR) {
        toaster_toasting = TRUE;
    } else {
        agt_record_error(
            scb,
            &msg->mhdr,
            NCX_LAYER_OPERATION,
            res,
            methnode,
            NCX_NT_NONE,
            NULL,
            NCX_NT_NONE,
            NULL);
    }
    /* added code ends here */

    return res;

} /* y_toaster_make_toast_invoke */

Example RPC Invoke Callback

This example demonstrates some API functions that are available to an RPC or YANG action callback function.

Example YANG Modele

module deluser {
  namespace "urn:vendor:yumaworks:deluser";
  prefix delu;

  revision 2024-02-02;

  rpc delete-user {
    description "Remove the user from the system";
    input {
      leaf user-name {
        type string {
          length "1..max";
        }
        mandatory true;
      }
    }
  }

}

The 'delete-user' invoke callback function calls a static function 'do_delete_user' to perform the following:

  • read the '/nacm/groups' configuration data

  • traverse each 'group' child entry found

    • search for the specified 'user-name'

    • if found, add the user-name to the YANG Patch edit list

  • if any edits, apply the YANG Patch to the server

Example YANG Data

  • The server has the following NACM groups: 'G1', 'G2', and 'G3'.

  • The user-name 'U1' is being deleted by the client

> delete-user user-name=U1

Example Invoke Callback Function

If the code is generated from 'yangdump-pro' then the file 'u_deluser.c' will contain the function 'u_delu_delete_user_invoke'.


/**
 * @brief Invocation phase callback for "<delete-user>" operation. (agt_rpc_method_t)
 *
 * Validation callback has passed at this point.
 * Call device instrumentation code in this function.
 *
 * @param scb session invoking the RPC operation.
 * @param msg message in progress for this <rpc> request.
 * The msg->rpc_input value node contains the input (if any).
 * It is a container matching the rpc/input node for the YANG rpc.
 * @param methnode XML node for the operation, which can be used
 * in error reporting (or ignored).
 * @return return status for the phase.
 *  - An error in validate phase will cancel invoke phase
 *  - An rpc-error will be added if an error is returned and
 * the msg error Q is empty
 */
status_t u_delu_delete_user_invoke (
    ses_cb_t *scb,
    rpc_msg_t *msg,
    xml_node_t *methnode)
{
    status_t res = NO_ERR;

    if (LOGDEBUG) {
        log_debug("\nStart SIL invoke rpc <delete-user> from module deluser");
    }

    val_value_t *errorval = NULL;
    val_value_t *inputval = agt_get_rpc_input(msg);
    if (inputval == NULL) return ERR_NCX_OPERATION_FAILED;

    val_value_t *v_user_name_val = NULL;
    const xmlChar *v_user_name = NULL;

    /* RPC Input: user-name */
    v_user_name_val = val_find_child(
        inputval,
        y_delu_M_delu,
        y_delu_N_user_name);
    if (v_user_name_val) {
        v_user_name = VAL_STRING(v_user_name_val);
        errorval = v_user_name_val;
    } else {
        res = ERR_NCX_MISSING_PARM;
    }

    /* invoke your device instrumentation code here */
    res = do_delete_user(scb, v_user_name);

    if (res != NO_ERR) {
        agt_record_error(
            scb,
            &msg->mhdr,
            NCX_LAYER_OPERATION,
            res,
            methnode,
            (errorval) ? NCX_NT_VAL : NCX_NT_NONE,
            errorval,
            (errorval) ? NCX_NT_VAL : NCX_NT_NONE,
            errorval);
    }

    /* No output nodes expected */

    return res;

} /* u_delu_delete_user_invoke */

Example do_delete_user Function

The function 'do_delete_user' is invoked from the callback function. This function does all the work and is not coupled to the RPC callback API.

/**
 * @brief Delete a user from NACM
 *
 * @param scb session invoking the RPC operation.
 * @return return status for the phase.
 */
static status_t
    do_delete_user (ses_cb_t *scb,
                    const xmlChar *username)
{
    status_t res = NO_ERR;
    ncx_cfg_t cfg_id = NCX_CFGID_RUNNING;
    const xmlChar *defpath = (const xmlChar *)"/nacm/groups";

    /* get the parent container of the NACM group entries */
    val_value_t *parentval = agt_val_get_data(cfg_id, defpath, &res);
    if (parentval == NULL) {
        if (LOGDEBUG) {
            log_debug("\nGet Data for %s failed (%s)",
                      defpath, get_error_string(res));
        }
        return res;
    }

    if (LOGDEBUG4) {
        int32 startindent = 0;
        log_debug_t lvl = LOG_DEBUG_DEBUG4;
        log_debug4("\nGot value for '%s':", defpath);
        val_dump_value(parentval, startindent, lvl);
        log_debug4_append("\n");
    }

    /* even though only child is 'group' use val_child_find
     * to make sure no siblings added later get checked
     */
    const xmlChar *child_modname = val_get_mod_name(parentval);
    const xmlChar *child_name = (const xmlChar *)"group";
    val_value_t *groupval =
        val_child_find(parentval, child_modname, child_name);

    if (groupval == NULL) {
        if (LOGDEBUG) {
            log_debug("\nNo NACM groups found");
        }
        val_free_value(parentval);
        return NO_ERR;
    }

    /* start a Patch Edit to hold the edits for deleting
     * the user-name from each group that it is found
     */
    const xmlChar *patch_id_str = (const xmlChar *)"mypatch-1";
    boolean system_edit = false;

    /* the skip_sil parameter MUST be true for YumaPro implemented
     * SIL code such as /nacm
     */
    boolean skip_sil = false;
    const xmlChar *comment = (const xmlChar *)"From remove_user RPC";

    uint32 edit_count = 0;
    agt_edit_parms_cb_t *parms_cb =
        agt_new_edit_parms(patch_id_str,
                           system_edit,
                           skip_sil,
                           comment);
    if (parms_cb == NULL) {
        val_free_value(groupval);
        return ERR_INTERNAL_MEM;
    }

    while ((groupval != NULL) && (res == NO_ERR)) {

        /* get the key leaf for the group */
        val_value_t *name =
            val_find_child(groupval,
                           val_get_mod_name(groupval),
                           (const xmlChar *)"name");
        if (name == NULL) {
            res = ERR_NCX_OPERATION_FAILED;
            continue;
        }

        /* get the first user-name value */
        val_value_t *userval =
            val_find_child(groupval,
                           val_get_mod_name(groupval),
                           (const xmlChar *)"user-name");

        while ((userval != NULL) && (res == NO_ERR)) {
            if (!xml_strcmp(username, VAL_STRING(userval))) {
                /* found the user so make an edit */
                ++edit_count;
                res = add_delete_user_edit(parms_cb, userval, edit_count);
                userval = NULL;  // done with this loop either way
                continue;
            }

            userval = val_child_next_same(userval);
        }

        groupval = val_child_next_same(groupval);
    }

    /* apply the patch if OK */
    if (res == NO_ERR) {
        if (edit_count > 0) {
            res = agt_apply_patch_edit(scb, parms_cb);
            if (res != NO_ERR) {
                if (LOGDEBUG) {
                    log_debug("\nApply patch failed (%s)",
                              get_error_string(res));
                }
            }
        } else if (LOGDEBUG) {
            log_debug("\nUser '%s' not found in any NACM group",
                      username);
        }
    }

    val_free_value(groupval);
    agt_free_edit_parms(parms_cb);

    return res;

}  /* do_delete_user */

Key API Functions Used:

The 'agt_val_get_data' function is used to retrieve the NACM configuration data. Refer to the Get Data API section for details.

The 'val_dump_value' function is used to display the NACM data in debug mode. Refer to the Tips For Debugging YANG Instrumentation for details.

The 'val_child_find' function is used to traverse the NACM data looking for the specified 'user-name'.

val_value_t *val_child_find(const val_value_t *parent, const xmlChar *child_modname, const xmlChar *child_name)

Find the child node for the specified child name and modname.

Parameters:
  • parent -- parent val to search

  • child_modname -- modname of child node (NULL to match any module)

  • child_name -- local-name of child node

Returns:

pointer to found child or NULL

The 'val_child_next_same' function is used to continue a loop through all the leaf-list instances, searching for the 'user-name' that matches.

val_value_t *val_child_next_same(val_value_t *curnode)

Get the next node of the same type.

Parameters:

curnode -- current list entry

Returns:

pointer to next list entry or NULL if none

Example add_delete_user_edit Function

The function 'add_delete_user_edit' is used to generate the YANG Patch edit for the user-name to delete. It is called from the 'do_delete_user' function.

/**
 * @brief Add a delete user edit to the YANG Patch in progress
 *
 * @param parms_cb Patch control block to use
 * @param msg message in progress for this <rpc> request.
 * @return return status for the phase.
 */
static status_t
    add_delete_user_edit (agt_edit_parms_cb_t *parms_cb,
                          val_value_t *userval,
                          uint32 edit_num)
{
    /* YANG patch internals use old YANG-API format */
    ncx_instfmt_t format = NCX_IFMT_YANGAPI;
    xmlChar *buff = NULL;

    status_t res =
        val_gen_instance_id (NULL, userval, format, &buff);
    if (res != NO_ERR) {
        return res;
    }

    /* make an edit-id string */
    char numbuff[NCX_MAX_NUMLEN+2];
    *numbuff = 0;
    snprintf(numbuff, NCX_MAX_NUMLEN-1, "E%u", edit_num);

    const xmlChar *edit_id_str = (const xmlChar *)numbuff;
    const xmlChar *edit_target = buff;
    const xmlChar *edit_operation = (const xmlChar *)"delete";

    /* these parameters must be NULL for a delete operation */
    const xmlChar *edit_xml_value = NULL;
    const xmlChar *insert_point = NULL;
    const xmlChar *insert_where = NULL;

    res = agt_add_patch_edit(parms_cb,
                             edit_id_str,
                             edit_target,
                             edit_operation,
                             edit_xml_value,
                             insert_point,
                             insert_where);

    m__free(buff);

    return res;

}  /* add_delete_user_edit */

Key API Functions Used:

The 'val_gen_instance_id' function is used to generate the path string for the YANG-API formatted string. Note the NCX_IFMT_YANGAPI format enumeration used in this special case. Refer to the Printing the Value Path String from a SIL Edit Callback section for more details.

The 'agt_add_patch_edit' function is used to add a 'delete' edit request for the 'user-name'. Refer to the Add Edits to Patch Edit Request section for details on this API function.

Example delete-user Invocation

The following log file trace shows the server activity for




ses_accept_input on session 3
ses read OK (221) on session 3
ses: accept buffer ssh/1.1 (221):

#211
<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="4"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <delete-user xmlns="urn:vendor:yumaworks:deluser">
  <user-name>U1</user-name>
 </delete-user>
</rpc>
##


ses_msg: allocate msg 0x55c539d8c640
ses_msg: reused in buff 0x55c539c021f0 for s 3
agt_ses msg ready for session 3
Incoming msg for session 3
<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="4"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <delete-user xmlns="urn:vendor:yumaworks:deluser">
  <user-name>U1</user-name>
 </delete-user>
</rpc>

agt_top: got node
XML node (16:0): START rpc
   attr: ns:4 name:xmlns (urn:ietf:params:xml:ns:netconf:base:1.0)
   attr: ns:0 name:message-id (4)

agt_top: start dispatch yuma-netconf:rpc
agt_rpc: <delete-user> for [email protected] (m:none) [2024-02-08T23:11:17Z]
XML node (54:1): START delete-user
   attr: ns:4 name:xmlns (urn:vendor:yumaworks:deluser)

agt_acm: check <delete-user> RPC allowed for user 'andy'
agt_acm: PERMIT (access-control off)
agt_val_parse: delu:input btyp:container
parse_complex: expecting start-child or end node.
XML node (54:2): START user-name

parse_string: expecting string node.
XML node (0:3): STRING
   val(2):U1

parse_string: expecting end for user-name
XML node (54:2): END user-name

parse_complex: expecting start-child or end node.
XML node (54:1): END delete-user

agt_rpc: parse RPC input state

 deluser:input {
   user-name U1
 }

agt_rpc: expecting 'rpc' end node
XML node (16:0): END rpc

add_default: checking 'deluser:input'
add_node_default: checking 'deluser:user-name'
agt_val_rpc_xpathchk: deluser:delete-user start
Checking for leafref backptrs for obj 'user-name'
run_instance_check: deluser:input start
instance_check 'deluser:user-name' against 'deluser:input'
    (cnt=1, min=1, max=1)
Start SIL validate rpc <delete-user> from module deluser
Start SIL invoke rpc <delete-user> from module deluser
agt_val: retrieving '/nacm/groups' from running datastore
xpath1: enter value walker for ietf-netconf-acm:nacm (0)
xpath1: enter value walker for ietf-netconf-acm:groups (0)
eval_expr
xpath value result for '/nacm/groups'
  typ: nodeset (size: 1) =
   node 1 (0x55c539c080a0) VALHDR nacm:groups (1)


Got value for '/nacm/groups':
groups {
  group  G1 {
    name G1
    user-name U1
    user-name U2
    user-name U3
  }
  group  G2 {
    name G2
    user-name U4
    user-name U1
  }
  group  G3 {
    name G3
    user-name U4
    user-name U1
    user-name U2
  }
}

Converted urlstring (/nacm/groups/group/G1/user-name/U1) to XPath (/nacm:nacm/nacm:groups/nacm:group[nacm:name="G1"]/nacm:user-name[.="U1"])


.... log details removed


***** start commit phase on running for session 3, transaction 11459 *****

Start full commit of transaction 11459: 3 edits on running config
Start invoking commit SIL callback for delete on ietf-netconf-acm:group
Enter ietf_netconf_acm_nacm_groups_group_edit callback for commit phase
Start key walk for user-name
End key walk for user-name:  retkey:

  name G1
Clearing user-2-group entries in ACM cache
Finished invoking user callback on ietf-netconf-acm:group
Start invoking commit SIL callback for delete on ietf-netconf-acm:group
Enter ietf_netconf_acm_nacm_groups_group_edit callback for commit phase
Start key walk for user-name
End key walk for user-name:  retkey:

  name G2
Clearing user-2-group entries in ACM cache
Finished invoking user callback on ietf-netconf-acm:group
Start invoking commit SIL callback for delete on ietf-netconf-acm:group
Enter ietf_netconf_acm_nacm_groups_group_edit callback for commit phase
Start key walk for user-name
End key walk for user-name:  retkey:

  name G3
Clearing user-2-group entries in ACM cache
Finished invoking user callback on ietf-netconf-acm:group
SIL-SA Commit Complete callbacks OK
edit-transaction 11459: on session 3 by [email protected]
  time: 2024-02-08T23:11:17Z
  message-id: --
  trace-id: --
  datastore: running
  operation: delete
  target: /nacm:nacm/nacm:groups/nacm:group[nacm:name="G1"]/nacm:user-name[.="U1"]
  comment: From remove_user RPC

edit-transaction 11459: on session 3 by [email protected]
  time: 2024-02-08T23:11:17Z
  message-id: --
  trace-id: --
  datastore: running
  operation: delete
  target: /nacm:nacm/nacm:groups/nacm:group[nacm:name="G2"]/nacm:user-name[.="U1"]
  comment: From remove_user RPC

edit-transaction 11459: on session 3 by [email protected]
  time: 2024-02-08T23:11:17Z
  message-id: --
  trace-id: --
  datastore: running
  operation: delete
  target: /nacm:nacm/nacm:groups/nacm:group[nacm:name="G3"]/nacm:user-name[.="U1"]
  comment: From remove_user RPC

Complete commit OK of transaction 11459 on running database
YP-HA: Active: skip update: not enabled
Writing <running> config to file '/home/andy/.yumapro/startup-cfg.xml'
agt_cb: Enter run_startup_hook
agt_ncx: Saving config with temp filename (/home/andy/.yumapro/startup-cfg.xml.temp)
xml_wr: skip xmlns duplicate (9=http://netconfcentral.org/ns/yuma-ncx)
ses: free scb: superuser@none sid: 0 fd: 0

Generating <netconf-config-change> notification
Queing <netconf-config-change> notification for stream NETCONF (cnt: 37)(id: 38)
Event Ancestor Keys: none
Event Payload:
  ncn:changed-by {
    username andy
    session-id 3
    source-host 127.0.0.1
  }
  ncn:datastore running
  ncn:edit {
    target /nacm:nacm/nacm:groups/nacm:group[nacm:name="G1"]/nacm:user-name[.="U1"]
    operation delete
  }
  ncn:edit {
    target /nacm:nacm/nacm:groups/nacm:group[nacm:name="G2"]/nacm:user-name[.="U1"]
    operation delete
  }
  ncn:edit {
    target /nacm:nacm/nacm:groups/nacm:group[nacm:name="G3"]/nacm:user-name[.="U1"]
    operation delete
  }
agt_cb: Enter run_trans_complete
agt_cb: Enter sa_run_trans_complete
Clearing current txid for running config
ses: free scb: [email protected] sid: 3 fd: 0

agt_rpc: sending ok <rpc-reply> for ses 3 msg '4'

ses_msg: send 1.1 buff:144 for s:3

trace_buff:

#134
<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply message-id="4"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <ok/>
</rpc-reply>
agt_audit: skip audit record for RPC summary
agt_cb: Enter run_command_complete
agt_top: end dispatch yuma-netconf:rpc
ses_msg: free msg 0x55c539d8c640 for session 3

RPC Data Output Handling

RPC operations can return data to the client if the operation succeeds. The YANG “output” statement defines the return data for each RPC operation. Constructing YANG data is covered in detail elsewhere. This section shows a simple example SIL invoke function that returns data.

The output data is usually constructed with APIs like agt_make_leaf from agt/agt_util.h.

To use these APIs, the output object is needed.

Starting in release 23.10-6, the following API is available:

obj_template_t *agt_get_rpc_output_obj(rpc_msg_t *msg)

Get the rpc output object template.

Parameters:

msg -- RPC message to check

Returns:

output object or NULL if some error

This function will provide the parent object for any data objects returned in the RPC output section.

The code generated for the RPC 'invoke' function will include the following code:

obj_template_t *outputobj = agt_get_rpc_output_obj(msg);
if (outputobj == NULL) return ERR_NCX_OPERATION_FAILED;

The 'outputobj' variable can be used as the 'parentobj' parameter for the 'agt_make_*' family of APIs.

For releases prior to 23.10-6, the following code can be used to get this object:

obj_template_t *obj = RPC_MSG_METHOD(msg);
if (obj) {
    obj = obj_find_child(obj, NULL, NCX_EL_OUTPUT);
}
if (obj == NULL) {
    return ERR_NCX_DEF_NOT_FOUND;  // should not happen
}

Once this object template is retrieved from the 'msg' parameter data can be added to the 'msg' using val_value_t structures.

The following snippet shows a leaf being created using an int32 value:

int32 result = 42;
val_value_t *val =
     agt_make_int_leaf(obj,
                       y_addrpc_N_sum,
                       result,
                       &res);

After the value is created it must be added to the message using the 'agt_rpc_add_return_val' API:

void agt_rpc_add_return_val(val_value_t *return_val, rpc_msg_t *msg)

Add a return value to the msg.

Parameters:
  • return_val -- value to add

  • msg -- message to add value into

if (val) {
   agt_rpc_add_return_val(val, msg);
}

RPC Data Output Example

Example YANG Module:

module addrpc {
  namespace "http://www.yumaworks.com/ns/addrpc";
  prefix add;
  revision "2020-02-25";

  rpc add {
    description "Get the sum of two numbers";
    input {
      leaf num1 {
        type int32;
        mandatory true;
        description "First number to add";
      }
      leaf num2 {
        type int32;
        mandatory true;
        description "Second number to add";
      }
    }
    output {
      leaf sum {
        type int32;
        mandatory true;
        description "The sum of the 2 numbers";
      }
    }
   }
}

Example SIL Invoke Function Returning Output Data:

/********************************************************************
* FUNCTION y_addrpc_add_invoke
*
* RPC invocation phase
* All constraints have passed at this point.
* Call device instrumentation code in this function.
*
* INPUTS:
*     see agt/agt_rpc.h for details
*
* RETURNS:
*     error status
********************************************************************/
static status_t y_addrpc_add_invoke (
    ses_cb_t *scb,
    rpc_msg_t *msg,
    xml_node_t *methnode)
{
    status_t res = NO_ERR;

    if (LOGDEBUG) {
        log_debug("\nStart SIL invoke rpc <add> from module addrpc");
    }

    val_value_t *inputval = agt_get_rpc_input(msg);
    if (inputval == NULL) return ERR_NCX_OPERATION_FAILED;

    val_value_t *v_num1_val = NULL;
    int32 v_num1 = 0;

    val_value_t *v_num2_val = NULL;
    int32 v_num2 = 0;

    v_num1_val = val_find_child(
        inputval,
        y_addrpc_M_addrpc,
        y_addrpc_N_num1);
    if (v_num1_val) {
        v_num1 = VAL_INT(v_num1_val);
    }

    v_num2_val = val_find_child(
        inputval,
        y_addrpc_M_addrpc,
        y_addrpc_N_num2);
    if (v_num2_val) {
        v_num2 = VAL_INT(v_num2_val);
    }

    /* remove the next line if scb is used */
    (void)scb;

    /* remove the next line if methnode is used */
    (void)methnode;

    /* invoke your device instrumentation code here */

    /* Following output nodes expected:
     * leaf sum
     */
    int32 result = v_num1 + v_num2;
    obj_template_t *obj = RPC_MSG_METHOD(msg);
    if (obj) {
        obj = obj_find_child(obj, NULL, NCX_EL_OUTPUT);
    }
    if (obj == NULL) {
        return ERR_NCX_DEF_NOT_FOUND;  // should not happen
    }

    val_value_t *val =
        agt_make_int_leaf(obj,
                          y_addrpc_N_sum,
                          result,
                          &res);
    if (val) {
        agt_rpc_add_return_val(val, msg);
    }

    return res;

} /* y_addrpc_add_invoke */

RPC Post Reply Callback Function

../_images/rpc_post_reply_phase.png

The RPC post-reply callback function is used to clean up after a message has been processed. Only 1 function can register for each YANG rpc statement. The standard NETCONF operations are reserved by the server engine. This callback is not needed unless the SIL validate or invoke callback allocated some memory that needs to deleted after the <rpc-reply> is sent.

The RPC post reply callback function is optional to use. It is enabled with the 'agt_rpc_register_method' function, within the phase 1 initialization callback function.

The yangdump-pro code generator will not create this SIL callback function by default.

Example SIL Function Registration

Example SIL Function:

/********************************************************************
* FUNCTION y_foo_command_post
*
* RPC post reply phase
*
* INPUTS:
*     see agt/agt_rpc.h for details
*
* RETURNS:
*     error status
********************************************************************/
static status_t
    y_foo_command_post (
        ses_cb_t *scb,
        rpc_msg_t *msg,
        xml_node_t *methnode)
{
    (void)scb;
    (void)methnode;
    if (msg->rpc_user1 != NULL) {
        m__free(msg->rpc_user1);
        msg->rpc_user1 = NULL;
    }
    return NO_ERR;
}  /* y_foo_command_post */

DataStore Edit APIs For RPC SIL Callbacks

Note

  • This feature is available starting in 23.10-2

  • The APIs in this section must only be used from within SIL callback code for YANG RPCs and actions

Usage

A custom RPC (or action) callback can edit the server configuration using APIs similar to the Database Edit APIs for DB-API subsystems.

  • These APIs must be used with care, especially the option to treat the datastore edit as a 'system edit'.

The APIs in this section follow the same pattern as the DB-API functions for Database Edits Using YANG Patch.

There are 4 steps needed to use this database editing method:

Restrictions

These APIs can be found in 'agt_util.h' and only used with the netconfd-pro process. They will not work if used from a different process.

There are not many restrictions for this mode of datastore editing. That is why its usage should be done with extreme caution. There are two parameters that require special attention:

  • The 'system_edit' flag should be set to 'false' in almost all cases. If 'true' then access control will be bypassed.

  • The 'skip_sil' flag should be set with great care. Any edit that changes data managed by server SIL callbacks must not set this flag to 'true'.

The edit transaction will cause an audit log update and config-change notification to be sent. This may be done before the <rpc-reply> for the original RPC request is done.

Start Patch Edit Request

The SIL callback must start a patch edit control block as the first step.

Dynamic Edit Parms

The edit parms control block can be malloced and then freed after the edit is applied the server.

  • Use agt_new_edit_parms to create

agt_edit_parms_cb_t *agt_new_edit_parms(const xmlChar *patch_id_str, boolean system_edit, boolean skip_sil, const xmlChar *comment)

Create and initialize a SIL edit_parms control clock.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

Parameters:
  • patch_id_str --

    string to use as the patch ID

    NULL to use the default patch-id field

  • system_edit --

    TRUE if this edit is from the system

    FALSE if this is a user edit subject to access control

  • skip_sil -- TRUE to skip, FALSE to not skip

  • comment -- comment string or NULL to use default

Returns:

malloced and initialized control block

Example:

const xmlChar *patch_id_str = (const xmlChar *)"MyPatch1";
const xmlChar *comment = (const xmlChar *)"myComment";

agt_edit_parms_t *parms =
    agt_new_edit_parms(patch_id_str,
                       false,   // system_edit
                       false,   // skip_sil
                       comment);

// .. Use the parms; check ERR_INTERNAL_MEM error if NULL

Static Edit Parms

The edit parms control block can be a static data structure that is cleaned after the edit is applied the server.

  • Use agt_init_edit_parms to initialize

status_t agt_init_edit_parms(agt_edit_parms_cb_t *parms, const xmlChar *patch_id_str, boolean system_edit, boolean skip_sil, const xmlChar *comment)

Initialize a SIL edit_parms control clock.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

Parameters:
  • parms -- [out] struct to initialize

  • patch_id_str --

    string to use as the patch ID

    NULL to use the default patch-id field

  • system_edit --

    TRUE if this edit is from the system

    FALSE if this is a user edit subject to access control

  • skip_sil -- TRUE to skip, FALSE to not skip

  • comment -- comment string or NULL to use default

Returns:

status

  • can fail if new patch PCB cannot be allocated

Example:

agt_edit_parms_t parms;
const xmlChar *patch_id_str = (const xmlChar *)"MyPatch1";
const xmlChar *comment = (const xmlChar *)"myComment";

status_t res =
    agt_init_edit_parms(&parms,
                        patch_id_str,
                        false,   // system_edit
                        false,   // skip_sil
                        comment);

// .. Use the parms; check ERR_INTERNAL_MEM error if NULL

Add Edits to Patch Edit Request

The 'agt_add_patch_edit' function is used to add edits to the YANG Patch edit to the edit transaction.

  • This must be done after an 'agt_edit_parms_t' struct is created or initialized.

  • This must be done before the 'agt_apply_patch_edit' function is invoked.

  • The 'system_edit' and 'skip_sil' parameters apply to all edits in the edit transaction, so be careful about combining edits in the same patch request.

  • The 'edit_id_str' parameter must be unique for each edit. The actual values are not important, but they are used as YANG key leaf values.

  • The 'edit_target' expression does not need prefixes if the referenced data nodes do not have any duplicate local-name sibling nodes.

status_t agt_add_patch_edit(agt_edit_parms_cb_t *parms, const xmlChar *edit_id_str, const xmlChar *edit_target, const xmlChar *edit_operation, const xmlChar *edit_xml_value, const xmlChar *insert_point, const xmlChar *insert_where)

Create an edit request and add to YANG Patch in progress.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

See also

db_api_add_edit

Parameters:
  • parms -- edit parms in progress

  • edit_id_str -- index value for the edit

  • edit_target -- edit target path string

  • edit_operation -- edit operation (create merge replace delete remove)

  • edit_xml_value --

    XML payload in string form, whitespace allowed

    MAY BE NULL if no value required (delete remove))

  • insert_point -- a string like the target except a different instance of the same list of leaf-list; only for before, after; NULL if not used

  • insert_where -- == <insert enum string> (may be NULL if not used)

    • "before"

    • "after"

    • "first"

    • "last"

Returns:

status

Example:

const xmlChar *edit_id_str = (const xmlChar *)"edit1";
const xmlChar *edit_target - (const xmlChar *)"/int8.1";
const xmlChar *edit_operation = (const xmlChar *)"merge";
const xmlChar *edit_xml_value = (const xmlChar *)
  "<int8.1 xmlns='http://netconfcentral.org/ns/test'>22</int8.1>";

status_t res =
   agt_add_patch_edit(parms,
                      edit_id_str,
                      edit_target,
                      edit_operation,
                      edit_xml_value,
                      NULL,    // insert_point
                      NULL);   // insert_where

Apply Patch Edit to Server

The 'agt_apply_patch_edit' is used to apply the edit transaction to the server after all the individual edits have been added.

  • The 'scb' parameter should be the one for the session that is invoking the RPC operation or YANG action.

This function can fail for many reasons:

  • Any datastore operation is already in progress

  • Any configuration datastore is not ready to be written

  • The 'edit_xml_value' string is provided and does not parse correctly, based on the 'edit_target' string

  • Any normal configuration validation error can occur depending on the edit request

status_t agt_apply_patch_edit(ses_cb_t *scb, agt_edit_parms_cb_t *parms)

SIL version of the db_api_send_edt_full2 API.

Create a YANG Patch edit request and invoke it locally. This patch can have exactly one edit. Provides full access to all 1-shot send_edit parameters.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

Parameters:
  • scb -- session control block for real session that is altering the datastore as a side effect of the RPC operation in progress

  • parms -- == agt_edit_parms_cb_t struct

    • MUST BE INITIALIZED FIRST WITH agt_init_edit_parms

Returns:

status

Example:

res = agt_apply_patch_edit(scb, parms);

Cleanup the Patch Edit

The patch edit parms are not needed after the edit is applied.

  • If a dynamic edit parms control block is created it must be freed.

  • If a static edit parms control block is initialized it must be cleaned.

Dynamic Edit Parms

  • Use agt_free_edit_parms to free the edit parms control block.

void agt_free_edit_parms(agt_edit_parms_cb_t *parms)

Clean and free a SIL edit_parms control clock.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

Parameters:

parms -- struct to clean and free

Example:

agt_free_edit_parms(parms);

Static Edit Parms

  • Use agt_clean_edit_parms to clean

void agt_clean_edit_parms(agt_edit_parms_cb_t *parms)

Clean a SIL edit_parms control clock.

THIS API MUST ONLY BE USED WITHIN THE MAIN SERVER.

Parameters:

parms -- struct to clean

Example:

agt_clean_edit_parms(&parms);

RPC Example: Add NACM Edit from a Custom RPC

This example demonstrates how these APIs can be used to update the NACM 'group' configuration when the custom RPC 'add-user' is invoked.

  • The client invokes the 'add-user' RPC and the /nacm/groups subtree is updated with the proper configuration.

YANG For the add-user RPC

rpc add-user {
  input {
    leaf group {
      type yang:yang-identifier;
      mandatory true;
    }
    leaf user {
      type yang:yang-identifier;
      mandatory true;
    }
  }
}

Prepare Steps

  • Make sure the YANG module 'test/pass/testrpc.yang' is present on the system and found by yangdump-pro.

  • Run the make_sil_dir_pro script to generate the stub code

    > make_sil_dir_pro testrpc
    
  • Add the 'add_nacm_user' function to the file 'u_testrpc.c'

  • Change the 'add_user_validate' and 'add_user_invoke' functions to extract the parameters and invoke the 'add_nacm_user' function

add-nacm_user Function

  • This function contains all the API examples from this section.

  • The NACMXML template is used to create a simple XML snippet to merge into the /nacm/groups subtree.

  • The last term in the 'edit_xml_value' string must match the top-level XML element in the 'edit_xml_value' parameter

  • Set the 'edit_xml_value' parameter to NULL if the 'edit_operation' string is equal to delete or remove

  • Use 'system_edit' set to false to make this a user edit.

  • Use 'skip_sil' set to false since the NACM module is implemented by the server and the SIL callbacks must be invoked to activate the configuration.

/**
 * @brief Use the SIL datastore edit APIs to add a NACM user
 *
 * @param group NACM group name
 * @param user NACM user name
 * @return return status
 */
static status_t
    add_nacm_user (ses_cb_t *scb,
                   const xmlChar *group,
                   const xmlChar *user)
{

    status_t res = NO_ERR;

    /* set parameters for this example edit
     * - treat as a user edit
     * - do not skip NACM SIL or group/user will not be used
     */
    const xmlChar *patch_id_str = (const xmlChar *)"test-rpc-patch";
    boolean system_edit = false;
    boolean skip_sil = false;
    const xmlChar *comment = (const xmlChar *)"add-user edit";

    /* step 1: start a YANG Patch edit */
    agt_edit_parms_cb_t *parms =
        agt_new_edit_parms(patch_id_str,
                           system_edit,
                           skip_sil,
                           comment);
    if (parms == NULL) {
        return ERR_INTERNAL_MEM;
    }

    /* step 2: add 1 or more edits
     * - do not need to check any existing config first for this edit
     * - picking the groups container as the start of the merge
     * - Using a strncpy buffer to fill a simple XML template
     */
#define XMLBUFFLEN 512
    char value_buff[XMLBUFFLEN];
    *value_buff = 0;

#define NACMXML \
    "<groups><group><name>%s</name><user-name>%s</user-name></group></groups>"

    /* create the edit_xml_value parameter first */
    int ret = snprintf(value_buff,
                       XMLBUFFLEN,
                       NACMXML,
                       (const char *)group,
                       (const char *)user);
    if (ret <= 0) {
        res = ERR_NCX_OPERATION_FAILED;
    }

    const xmlChar *edit_id_str = (const xmlChar *)"E1";
    const xmlChar *edit_target = (const xmlChar *)"/nacm/groups";
    const xmlChar *edit_operation = (const xmlChar *)"merge";
    const xmlChar *edit_xml_value = (const xmlChar *)value_buff;
    const xmlChar *insert_point = NULL;
    const xmlChar *insert_where = NULL;

    if (res == NO_ERR) {
        res = agt_add_patch_edit (parms,
                                  edit_id_str,
                                  edit_target,
                                  edit_operation,
                                  edit_xml_value,
                                  insert_point,
                                  insert_where);
    }

    if (res == NO_ERR) {
        /* Step 3: Apply the edit */
        res = agt_apply_patch_edit(scb, parms);
    }

    /* step 4: cleanup */
    agt_free_edit_parms(parms);

    return res;

}  /* add_nacm_user */

RPC Validate Function Changes

The 'u_trpc_add_user_validate' function is altered to check the maximum group and user name string lengths.

     /* RPC Input: group */
     v_group_val = val_find_child(
         inputval,
         y_trpc_M_trpc,
         y_trpc_N_group);
     if (v_group_val) {
         v_group = VAL_STRING(v_group_val);
         if (xml_strlen(v_group) > NCX_MAX_NCXNAME_LEN) {
             res = ERR_NCX_TOO_BIG;
             errorval = v_group_val;
         }
     }

     /* RPC Input: user */
     if (res == NO_ERR) {
         v_user_val = val_find_child(
             inputval,
             y_trpc_M_trpc,
             y_trpc_N_user);
         if (v_user_val) {
             v_user = VAL_STRING(v_user_val);
             if (xml_strlen(v_user) > NCX_MAX_NCXNAME_LEN) {
                 res = ERR_NCX_TOO_BIG;
                 errorval = v_user_val;
             }
         }
     }

RPC Invoke Function Changes

The 'u_trpc_add_user_invoke' function is altered to invoke the 'add_nacm_user' function to apply the NACM edit.

/* invoke your device instrumentation code here */
res = add_nacm_user(scb, v_group, v_user);

RPC Invocation Example

In this example, the client is invoking add-user for group 'G1' and user 'U1'.

> add-user group=G1 user=U1

The XML for this request may look as follows:

<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="3"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <add-user xmlns="urn:yumaworks:params:xml:ns:yang:testrpc">
  <group>G1</group>
  <user>U1</user>
 </add-user>
</rpc>

The server log shows the complete transaction (e.g. same as an <edit-config> operation), and if successful, will include a section from 'agt_yangapi_edit' similar to the following:

agt_yangapi_edit: <config> content:

  yuma-netconf:config {
      ietf-netconf-acm:nacm {
          groups {
              group  G1 {
                  name G1
                  user-name U1
              }
          }
      }
  }

Example Audit Log Entry

If successful, there will may be an audit log entry for the added edit, similar to the following:

edit-transaction 9384: on session 3 by [email protected]
  time: 2023-10-23T01:21:49Z
  message-id: --
  trace-id: --
  datastore: running
  operation: create
  target: /nacm:nacm
  comment: add-user edit

Example Successful Response

If successful, the 'rpc-reply' for the 'add-user' request will be sent back to the client.

<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply message-id="3"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <ok/>
</rpc-reply>

The 'running' configuration returned from a <get-config> operation will contain the newly added configuration, which may look as follows:

<data>
  <nacm xmlns="urn:ietf:params:xml:ns:yang:ietf-netconf-acm">
   <groups>
    <group>
     <name>G1</name>
     <user-name>U1</user-name>
    </group>
   </groups>
  </nacm>
 </data>

Example Error Response

In this example, the 'candidate' datastore is locked by another user which will cause the 'add-user' operation to fail. In this case an 'rpc-error' will be returned, that may appear as follows:

<rpc-reply message-id="3" xmlns:ncx="http://netconfcentral.org/ns/yuma-ncx"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <rpc-error>
  <error-type>rpc</error-type>
  <error-tag>in-use</error-tag>
  <error-severity>error</error-severity>
  <error-app-tag>no-access</error-app-tag>
  <error-path>/add-user</error-path>
  <error-message xml:lang="en">config locked</error-message>
  <error-info>
   <error-number>301</error-number>
  </error-info>
 </rpc-error>
</rpc-reply>