Schema Mount Server APIs

The Schema Mount feature allows entire YANG modules to be mounted within another module, similar to augments, but much more powerful and flexible than augments.

This feature is simple to understand but requires significant complexity to implement in the YANG compiler and the server automation.

First Available: 23.10

Schema Mount Aware Server Code

There are several design considerations for deploying complex Schema Mount YANG module instrumentation.

A schema mount "location" is either the top-level or a specific mount-point root supported by the server.

Note

  • Each location has its own modules, revisions, features, deviations, and annotations.

  • There must be SIL callbacks installed for all ancestors of any mounted modules in order for mounted SIL callbacks to be invoked.

Schema Mount Design Principles

The "root" data node of a YANG datastore is used to represent the entire configuration or operational state of a server. There are no actual child nodes defined. Instead, all top-level YANG data nodes (from the YANG modules loaded into the server) are available as child nodes of the root.

The Schema Mount feature allows a configuration container or list node to be defined as a mount point, and used like the root node. There are no actual child nodes (except list key leafs) defined in a mount point. Instead, all top-level YANG data nodes (from the YANG modules specified in the sm-config Structure are available as child nodes of the root. There may be many instances of the mount point, since it (or an ancestor) can be a YANG list.

The design for SIL and SIL-SA code for mounted modules does not need to change. The code is generated and written as if it is located at the top-level. The Schema Mount framework allows YANG modules and related source code to be reused. The server APIs have been enhanced to no SIL or SIL-SA code changes are needed to implement a YANG module within a mount point instead of the top-level.

A Schema Mount Aware library needs to know where it is mounted. It is very simple to convert existing code (or create new code) to utilize the mount point instance (MPID) information. Refer to the Tutorial: Schema Mounted Virtual Toaster section for an example.

Optional APIs are needed for Schema Mount Aware instrumentation:

  • A new file "ncx/sm.h" is now available (starting in the 22.10T train).

  • The APIs can often be used even if WITH_SCHEMA_MOUNT is not set.

    In that case, all functions return NULL or an error.

  • Top-level SIL and SIL-SA code is not required to know anything about the specific mounted modules under a mount point. In fact stub code SIL code can be used in most cases for a module containing the mount point. SIL code must be installed for the mounted module SIL code to be invoked during edit operations.

  • The mounted modules usually need to know about the top-level module containing the mount point.

SIL and SIL-SA API calls that use path strings will have different forms:

  • The SIL-SA sub-system does not store modules in a hierarchy like the main server. Instead all API calls will be relative to the mount point root.

  • Extra path information has to be derived from the mount point instance information available to the callback function.

SIL and SIL-SA implementation restrictions apply:

  • A YANG module can appear once in each location. The revision, features, and deviations. can be different in each location.

  • Each subsystem can only implement a YANG module once.

  • A module can be implemented in multiple subsystems.

SIL and SIL-SA callbacks can both be loaded for the same objects, RPC operations and actions:

  • When a SIL library is loaded, the server will check the YANG library configuration for each location to determine if that module/revision is needed.

  • If a module is needed in a location, then the server will invoke the SIL "init1" callbacks, which will register the needed callbacks.

  • If multiple SIL libraries register for the same object, then the last one wins.

  • Do not use any modules a bundle or stand-alone SIL or SIL-SA which register for any object implemented by the server.

  • During subsystem registration, the "sil-sa-app" will determine if a module is needed in the subsystem. If so, then the server will invoke the SIL-SA "init1" callbacks, which will register the needed callbacks with the server.

  • Each SIL-SA subsystem should only register callbacks for one location.

  • It is possible for a SIL-SA subsystem to register callbacks for multiple locations, as long as no module is present in more than one location.

SIL and SIL-SA Code Generation

There are currently no additional APIs generated by yangdump-pro for Schema Mount. They must be added by existing SIL or SIL-SA hand at this time.

All of the existing auto-generated code is usable within a mount-point location, and none of it has to be changed.

The auto-generated SIL codestubs can be used directly with schema mount.

  • The SIL and SIL-SA callbacks for mounted modules are essentially the same as if the module is used in the top-level.

  • The path strings in the auto-generated SIL or SIL-SA code is always relative to a "location".

    • The path strings are the same whether the code is used at the top level or within a mount point.

  • There are some additional APIs added that may assist the SIL callback functions.

  • A "schema-mount aware" SIL callback function can access the "mount point instance" information.

    • For example, a module mounted under the ietf-interfaces "interface" list entry might need to know the interface name for the mount point instance.

  • A YANG module can be loaded in more than one location.

  • The module or bundle will be loaded into each location that is requested (i.e., top-level and/or 1 or more mount point identifiers).

The following SIL functionality is currently provided:

  • RPC callbacks

  • YANG Action callbacks

  • EDIT1, EDIT2 and EDIT3 callbacks

  • GET2 callbacks

  • Send notification APIs

  • Modules and Bundles can be used

The following SIL-SA functionality is currently provided:

  • RPC callbacks

  • YANG Action callbacks

  • EDIT1, EDIT2 and EDIT3 callbacks

  • GET2 callbacks

  • Modules and bundles can be used

Using a YANG Module in Multiple Locations

Each location is independent, but within the server deployment there are implementation restrictions and considerations for using the same YANG module.

Note

A library for a top-level module or bundle MUST NOT include any SIL callbacks for any modules that are already built into the netconfd-pro server.

It is not possible to provide your own top-level instrumentation to replace built-in server modules. Any such modification is not supported.

It is possible to provide your own SIL or SIL-SA libraries for mounted modules that already exist in the server, except ietf-yang-library.yang.

SIL Library Static Data is Shared

When a SIL library is loaded into the server, the 'static' data declared in any C modules is allocated only once. The same static data is accessed if the library is used in multiple locations.

It may not be safe to invoke SIL "init", "init2" and "cleanup" callbacks if they are invoked more than once.

Using Separate Libraries in Each Location

The safest approach is to create a separate bundle for each location. This creates a separate library with its own static data. Each bundle has a different name, but the modules can overlap or even be the same as another bundle in another location. The APIs are safe in this case because they are only invoked once.

Using the Same Library in Each Location

It is possible for a SIL library to be used in multiple locations, but coding restrictions apply.

Warning

If a SIL library is used in multiple locations, the static data is shared by all locations. Any static data added to the SIL code must account for this restriction.

For example, the standard init_static_vars functions in the SIL code with overwrite the previous values each time it is invoked. This may causes memory leaks or server crashes.

The callback registration code must be invoked in each location. The schema tree for each location must be setup with the required SIL callbacks.

The server will assign SIL or SIL-SA callbacks to all locations where a given module is registered. The SIL code must use MPID information to determine the location for any protocol operation using the SIL callbacks.

The same SIL or SIL-SA library code will be invoked multiple times if multiple locations are found. There are important changes to the SIL or SIL-SA code that must be made in order for this to work.

Note

The SIL or SIL-SA entry point code must be updated to support usage in multiple locations:

  • All SIL External Interface functions are affected

  • The 'init_static_vars' and other code must be changed to insure it only runs once, even if invoked multiple times

  • All callback registration and unregistration must be done each time the callback is invoked.

Enabling Schema Mount

The schema mount feature has to be enabled in the system to be used. The --with-schema-mount and --sm-config CLI parameters control this behavior in the server.

In all other tools schema mount is enabled if WITH_SCHEMA_MOUNT=1 is used in the build.

The following API can be used to determine if schema mount is enabled:

boolean ncx_get_sm_enabled(void)

Get schema mount enabled flag.

On the server this flag will be set to false if there is no sm-config provided. This will save time and memory by ignoring mount-points that have no chance of being used.

Returns:

TRUE ifr schema mount is enabled

Example YANG Module With a Mount Point

The following example module defines a simple mount point:

  • Module: vtoaster

  • Label: toaster

  • Path: /vtoasters/vtoaster

  • Ancestor Keys: id

module vtoaster {
  yang-version 1.1;
  namespace "urn:yumaworks:params:xml:ns:vtoaster";
  prefix vtoast;

  import ietf-yang-schema-mount {
    prefix yangmnt;
  }

  revision 2023-02-20 {
    description "Initial version.";
  }

  container vtoasters {
    list vtoaster {
      yangmnt:mount-point "toaster";
      key id;
      leaf id {
        type uint8 {
          range "1 .. max";
        }
        description
          "Identifier for each virtual toaster.";
      }
    }
  }

}

Schema Mount Root Control Block

A root control block (rootcb) is used to maintain the configuration and state data associated with a specific mount point identifier (module name and mount point label).

  • The 'ncx_sm_rootcb_t' struct is used to manage a root control block. It can be found in ncx/ncxtypes.h.

  • This structure should not be accessed directly. Use the API macros and functions described below.

  • There are no vendor-facing APIs for this data structure other than to use them to access the mount point instances (MPIDs) within the MP.

ncx_sm_rootcb_t Access

The following macros are defined in ncx/ncx.h The parameter R is a pointer to a ncx_sm_rootcb_t struct.

/* access label in rootcb */
#define ROOTCB_LABEL(R) (R)->label

/* access modname in rootcb */
#define ROOTCB_MODNAME(R) (R)->modname

/* access objpath field */
#define ROOTCB_OBJPATH(R) (R)->objpath

/* access rootobj field */
#define ROOTCB_ROOTOBJ(R) (R)->rootobj

The ncx/ncx.c and ncx/sm.c modules maintain the main schema mount data structures. The following functions can be used to access this list of ncx_sm_rootcb_t structs.

dlq_hdr_t *ncx_get_rootQ(void)

Get the Root control block queue.

Returns:

Queue of root control blocks or NULL

ncx_sm_rootcb_t *ncx_first_rootcb(void)

Get the first Mount Point Control Block Entry in the rootcbQ.

Returns:

Mount Point Entry if found or NULL if not

ncx_sm_rootcb_t *ncx_next_rootcb(ncx_sm_rootcb_t *rootcb)

Get the next Mount Point Entry in the rootcbQ.

Parameters:

rootcb -- current Mount Point Entry

Returns:

Mount Point Control Block Entry if found or NULL if not

void sm_dump_rootcb(ncx_sm_rootcb_t *rootcb)

Print the interesting fields in Root Control block Must be debug for some info, debug4 for full info.

—log-level=debug or higher

Parameters:

rootcb -- Root Control Block to output

The following example shows possible output for sm_dump_rootcb:

Mount Point #1
  Mount Point Parameters:
   modname: vtoaster
   label: toaster
   root: /vtoast:vtoasters/vtoast:vtoaster
   config: true
   active: true
    module: 'ietf-inet-types'
    module: 'ietf-yang-library'
    module: 'ietf-yang-types'
    module: 'toaster'
void sm_dump_all_rootcb(void)

Print the interesting fields in all rootcbs.

In order to support yangcli-pro and netconfd-pro with the same code base, there is a "current rootcb" maintained in ncx/ncx.c.

ncx_sm_rootcb_t *ncx_get_cur_rootcb(void)

Gte the current root control block value.

Get the current root control block value

Returns:

rootcb if any

void ncx_set_cur_rootcb(ncx_sm_rootcb_t *rootcb)

Set the current root control block.

Set the current root control block value

Parameters:

rootcb -- Root control block to set as current

There is a rootcb created for each mount-point that matches a mount point identifier, found in the config file specified in the --sm-config CLI parameter.

The rootcb is stored in the obj_template_t for the container or list of the mount point object. The following functions can be used with the obj_template_t struct:

boolean obj_is_mp_with_rootcb(const obj_template_t *obj)

Check if current object is Mount Point object.

Check if current object is Mount Point object and its rootcb if setup correctly

Parameters:

obj -- object to check

Returns:

TRUE if this object is a Mount Point object with rootcb; FALSE otherwise

ncx_sm_rootcb_t *obj_find_ancestor_rootcb(obj_template_t *obj)

Find an ancestor node or self that has a rootcb.

Parameters:

obj -- object to check

Returns:

pointer to rootcb if found, NULL otherwise

boolean obj_is_mounted(obj_template_t *child)

Check if mounted object.

Check if this object is a mounted child for particular Mount Point

Parameters:

child -- object to check

Returns:

TRUE if this object is mounted object; FALSE otherwise

ncx_sm_mpid_t Access

An MPID contains the internal data structures to manage the instance of the mount point root. It also contains the fields such as the list of ancestor keys.

The MPID is stored in the 'val_extra' struct within the val_value_t struct for the container or list instance for the mount point root.

The following API in ncx/val.h can be used to access the MPID in a value node:

ncx_sm_mpid_t *val_get_mpid(const val_value_t *val)

Get the MPI.

Get the back-ptr to the Moint Point ID struct

Parameters:

val -- value to check

Returns:

back-ptr to the Moint Point ID struct

Note

  • SIL and SIL-SA code is always written as if it is mounted at the top-level.

  • The MPID for a mounted data node must be accessed to determine instance information for nodes above the mount point.

A schema mount aware callback function can access the mount point object and any ancestor key leafs needed to identify one data node instance of the mount point root.

struct ncx_sm_mpid_t

Moint Point Instance This struct lives in a val_value_t.val_extra struct.

It is created when the val_value_t is parsed and created This is used by internal code and SIL code to identify the ancestor keys above the mount point.

Public Members

dlq_hdr_t qhdr

queue header

dlq_hdr_t anckeyQ

Q of malloced val_value_t; 1 entry for each key leaf includes the ancestor keys.

struct val_value_t_ *valroot

Backptr to the value node that contains this MPID This is a SIL only field.

The SIL-SA version will be NULL

ncx_backptr_t backptr

inline backptr used to store this MPID in the rootcb->mpidQ.

The rootcb is stored in the obj_template_t for the SM root object. This is not used in SIL-SA.

boolean backptr_queued

flag to indicate the backptr is actually in a rootcb->mpidQ.

This is not used in SIL-SA.

xmlChar *module

Set for SIL-SA ONLY: module name for the mount point This is a malloced string.

xmlChar *label

Set for SIL-SA ONLY: label for the mount point This is a malloced string.

xmlChar *objpath

Set for SIL-SA ONLY: path string for the object template This is a malloced string.

The Mount Point root control block can be accessed with the following API:

ncx_sm_rootcb_t *sm_get_rootcb_for_mpid(ncx_sm_mpid_t *mpid)

Get the rootcb for an MPID.

Should work on SIL. May not work on SIL-SA

Parameters:

mpid -- MPID to check

Returns:

pointer to the rootcb or NULL if cannot be accessed

The following API can be used in "debug" mode to output MPID information to the server log:

void sm_dump_mpid(const ncx_sm_mpid_t *mpid)

Dump the contents of a MPID struct if DEBUG.

This is an external API that can be used in SIL code

Non-const needed for val_dump_value; not modified

Parameters:

mpid -- Mount Point ID control block to dump

The following example shows possible output for sm_dump_mpid if "debug4" log-level is used:

sm: MPID info
  MP valroot: toaster
  MP module: vtoaster
  MP label: toaster
  MP objpath: /vtoast:vtoasters/vtoast:vtoaster
  anckeyQ (1):
  id 2

The following APIs can be used in retrieve the MPIDs for a given rootcb:

ncx_sm_mpid_t *sm_get_first_mpid(ncx_sm_rootcb_t *rootcb)

Get the first MPID struct in the rootcb.

Parameters:

rootcb -- root control block to check

Returns:

pointer to first MPID or NULL if none

ncx_sm_mpid_t *sm_get_next_mpid(ncx_sm_mpid_t *curmpid)

Get the next MPID struct in the rootcb.

Parameters:

curmpid -- current MPID to get next for

Returns:

pointer to next MPID or NULL if none

The following APIs can be used in retrieve the ancestor keys from an MPID:

val_value_t *sm_get_first_anckey(const ncx_sm_mpid_t *mpid)

Get first ancestor key from the MPID.

Parameters:

mpid -- Mount Point Instance Descriptor to use

Returns:

pointer to the first entry or NULL if empty Q

val_value_t *sm_get_next_anckey(const val_value_t *curkey)

Get next ancestor key from the MPID.

Parameters:

curkey -- current kley entry

Returns:

pointer to the next entry or NULL if none

The following code snippet shows how MPID key leafs can be accessed:

/**
* @brief Compare MPID ancestor keys
*
* @param mpid1 test MPID 1
* @param mpid2 test MPID 2
* @return compare result
*/
static int32
    compare_mpid_keys (ncx_sm_mpid_t *mpid1,
                       ncx_sm_mpid_t *mpid2)
{
    val_value_t *key1 = sm_get_first_anckey(mpid1);
    val_value_t *key2 = sm_get_first_anckey(mpid2);
    while ((key1 != NULL) && (key2 != NULL)) {
        int32 ret = val_compare(key1, key2);
        if (ret != 0) {
            return ret;
        }

        key1 = sm_get_next_anckey(key1);
        key2 = sm_get_next_anckey(key2);
    }
    if ((key1 == NULL) && (key2 == NULL)) {
        return 0;
    }
    return (key1 == NULL) ? -1 : 1;

}  /* compare_mpid_keys */

Schema Mount APIs for EDIT Operations

All EDIT2 Callback and EDIT3 Callback transactions are conducted the same for a schema-mounted module.

The server handles fully integrated schema tree access from the top data node to the leafs in a mounted module. The top-level objects in the mounted modules are just child nodes of the mount point object.

Note

The registration and unregistration of EDIT callbacks is done exactly as for the top-level. The path statements are relative to the mount point root, not the top-level root.

All YANG validation and XPath evaluation from a mounted data node are done relative to the mount point. The MPI data node is used as the document root, not the real root.

Note

  • All edits for mounted configuration data are handled with top-level operations (e.g., <edit-config>).

  • Mounted data can be included in an edit <config> element just like any other data nodes.

  • Edit operations to create the MPI node and/or any mounted data nodes can be done separately or all at once.

The SIL or SIL-SA callback code only needs to access the MPID for a specific edit operation if it is schema mount aware. The following API can be used for this purpose, e.g., for the 'newval' or 'curval' in an edit transaction "undo" record.

ncx_sm_mpid_t *sm_get_ancestor_mpid(val_value_t *val)

Get the MPID for the specified mounted node.

This is an API for SIL or SIL-SA callback code

Parameters:

val -- value to check

Returns:

back-ptr to the MPID fot the MP ancestor or NULL if none or some error

The following example shows how some SIL callback code might use these functions:

/* get the ancestor MPID from a mounted configuration data node */
ncx_sm_mpid_t *mpid = sm_get_ancestor_mpid(newval);

if (mpid != NULL) {
    /* get the mount point rootcb */
    ncx_sm_rootcb_t *rootcb = sm_get_rootcb_for_mpid(mpid);
}

Schema Mount APIs for GET2 Operations

The GET2 Callback function works the same for mounted modules as it does for top-level modules. Only the ancestor nodes (and list key leafs) for the data nodes within the mount point are accessible through the normal GET2 API functions.

Note

The registration and unregistration of GET2 callbacks is done exactly as for the top-level. The path statements are relative to the mount point root, not the top-level root.

The MPID in use is available to a GET2 callback using the following macro:

/* Schema mount MPID field
 * The parameter G is a getcb_get2_t *
 */
#define GETCB_GET2_SM_MPID(G)  (G)->sm_mpid

The 'sm_mpid' fields will be set to a pointer to the ncx_sm_mpid_t struct for the ancestor mount point root for the "obj" in the GET2 callback. It will be set to 'NULL' if there is no ancestor MPID. The new toaster SIL module retrieves the toaster 'id' from the MPID and maintains an array of toasters instead of 1 toaster.

status_t u_toast_toasterStatus_get (
    getcb_get2_t *get2cb)
{
    // ....

    /* get the ancestor MPID node (if any) for this object */
    status_t res = NO_ERR;
    uint8 id = 0;
    ncx_sm_mpid_t *mpid = GETCB_GET2_SM_MPID(get2cb);
    if (mpid != NULL) {
        res = get_id_from_mpid(mpid, &id);
        if (res != NO_ERR) {
            return res;
        }
    }

    // ...
}

Schema Mount APIs for RPC Operations and Actions

An RPC operation in a mounted module is available as an "action" in the top-level server. Within the server the RPC and action callback APIs are different, so a mounted RPC still looks like an RPC operation to the SIL or SIL-SA code.

The only difference is that new optional APIs are available to identify the mount point instance. To retrieve the MPID information provided in the message input from the client:

Note

The registration and unregistration of RPC or action callbacks is done exactly as for the top-level. The path statements for actions are relative to the mount point root, not the top-level.

ncx_sm_mpid_t *sm_get_mpid_from_msg(rpc_msg_t *msg)

Get the MPID struct from the message header if set.

This field is set by the server during RPC or action processing so SIL code in schema mounted modules can identify which mount point instance is being used by the client

Parameters:

msg -- RPC msg message header to check

Returns:

pointer to the MPID struct if any set

This function provides a parsed MPID, if any. It is not verified to be correct or match an existing MPID in the server.

RPC Example

The following "invoke callback" shows how the new APIs can be used.

First, add an new include statement to the SIL module:

#include "sm.h"

Next, add code to get and display the mount point instance information.

 status_t u_t60_rpc1_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 <rpc1> from module t60");
     }

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

     ncx_sm_mpid_t *mpid = sm_get_mpid_from_msg(msg);
     if (mpid && LOGDEBUG) {
         sm_dump_mpid(mpid);
     }

     val_value_t *v_src_addr_val = NULL;

     // ....
 }

Schema Mount APIs for Notifications

NETCONF notification delivery can be used from a mounted module. This applies to top-level notifications and nested notifications.

  • A notification "send" function SHOULD pick a mount point instance to be the ancestor nodes used in the notification. Use APIs described above to access the MPIDs in use for a given mount point root.

  • If no MPID is selected then the first MPID found will be used.

Note

If no Mount Point Instances exist for a mounted module, but the SIL or SIL-SA code queues a notification anyway, then the notification will be dropped and not sent to any subscriptions.

Selecting an MPID to Use

The "send" function is called from system code. It is never called from the server since it is not a callback. Therefore there is no MPID for the server to provide to a notification send function. It must know or find the correct MPID to use in the notification.

  • The MPID is used by the server to generate the ancestor key values for the notification message.

  • If there are no ancestor keys because the mount point has no ancestor lists (only one or more containers), then no MPID needs to be set.

  • If only only instance of the mount point exists then the default (first entry) will be the only entry so no MPID needs to be set.

  • In most cases the "send" function will need internal state information to know the correct instance associated with the notification event.

Getting the RootCB for the Notification

If the notification is used in multiple locations with the same library, the the correct mount point root must first be determined. The :ref"ncx_sm_rootcb_t Access APIs can be used to find the correct root control block to use.

The notification object is saved in static data by the initialization function. If not then the "send" function must find it somehow to determine the correct mount point for the notification.

If the notification is used in only one location then there will be only one mount point root for it. In this case the "obj_find_ancestor_rootcb" API can be used to find the correct rootcb.

The following API can be used to set the MPID value in the notification before it is queued for delivery.

status_t agt_not_set_notif_mpid(agt_not_msg_t *msg, const ncx_sm_mpid_t *mpid)

Set the MPID to use for sending the schema-mounted notification.

The MPID is copied. It could be a real MPID from a valroot or a fake MPID constructed for this API

Not needed if there is only one instance of the mount point. Needed to provide the ancestor keys to use in the notification

Parameters:
  • msg -- message to set the MPID for

  • mpid -- MPID structure to clone and save. A copy will be made so this mpid can be deleted after this call

Returns:

status

Example: Searching the Ancestor MPIDs

In this example one mount point is assumed and the MPIDs for the rootcb are searched. An MPID could also be constructed. This is not supported with direct APIs at this time.

    agt_not_msg_t *notif = agt_not_new_notification(not2_obj);
    if (notif == NULL) {
        log_error("\nError: malloc failed, cannot send "
        "<not2> notification");
        return;
    }

#ifdef WITH_SCHEMA_MOUNT
    ncx_sm_rootcb_t *rootcb = obj_find_ancestor_rootcb(not2_obj);
    if (rootcb != NULL) {
        boolean found = false;
        ncx_sm_mpid_t *mpid = sm_get_first_mpid(rootcb);
        for (; mpid; mpid = sm_get_next_mpid(mpid)) {
            /* examine the ancestor keys
             * The SIL code must know what to expect here
             */
            val_value_t *keyval = sm_get_first_anckey(mpid);
            for (; keyval; keyval = sm_get_next_anckey(keyval)) {
                /* Use val_compare to check value not shown here
                 * set done=true if MPID is the one to use
                 */
            }

            /* if found then set the mpid and break */
            if (found) {
                /* this function can fail; should check status */
                (void)agt_not_set_notif_mpid(notif, mpid);
                break;
            }
        }
    }
#endif  // WITH_SCHEMA_MOUNT

Toaster SIL Code Modifications

The toaster module shows a special case example where the MPID used in an RPC operation needs to be remembered so a notification can be sent later.

The toaster.c example code has been updated to be Schema Mount aware. Note that this SIL code has been updated to the current API set. The contents of "toaster.c" have been migrated to "u_toaster.c".

The following changes are made to support schema mount:

  • Include "sm.h" in the C file

  • Make sure make command and Makefile properly sets WITH_SCHEMA_MOUNT

  • The toaster state has been moved from one instance to an array of structures.

  • A variable called toaster_mpid is added to the state of each toaster

  • A clone of the MPID used in the "make-toast" operation is saved for the "toastDone" notification. The real MPID cannot be saved. It can only be used within the RPC message.

    • The sm_clone_mpid API must be used so the MPID can be saved**

  • The MPID is added to the "toastDone" notification when it is generated

  • The saved MPID is freed when the notification is sent or when the module cleanup API is invoked

  • If a client accesses any object or RPC operation then it checks for an MPID.

    • If none, then the top-level toaster (ID=0) is used.

    • If a valid ID (1 .. 8) is found, then the proper toaster state is referenced.

    • If an invalid ID is found, then a ERR_NCX_NO_INSTANCE error is returned to the client.

Get the toaster ID from the MPID:

The 'vtoaster' list is indexed by a 'uint8' key leaf, called 'id'. The 'get_id_from_mpid' function shows how the 1 expected key can be accessed.

static status_t
    get_id_from_mpid (ncx_sm_mpid_t *mpid,
                      uint8 *retnum)
{
    if ((mpid == NULL) || (retnum == NULL)) {
        return ERR_NCX_OPERATION_FAILED;
    }

    val_value_t *keyval = sm_get_first_anckey(mpid);
    if (keyval == NULL) {
        return ERR_NCX_MISSING_PARM;
    }

    if (VAL_TYPE(keyval) != NCX_BT_UINT8) {
        return ERR_NCX_INVALID_VALUE;
    }

    uint8 num = VAL_UINT8(keyval);

    if ((num == 0) || (num > TOASTER_CNT)) {
        return ERR_NCX_NO_INSTANCE;
    }

    *retnum = num;
    return NO_ERR;

} /* get_id_from_mpid */

Save the MPID:

The "make-toast" operation callback function must save the MPID used by the client, so it can be used for the "toastDone" notification sent after the toast is done or cancelled.

Warning

Any MPID that is accessed from a message or val_value_t must be cloned to be used after the callback function is finished.

ncx_sm_mpid_t *sm_clone_mpid(const ncx_sm_mpid_t *mpid, val_value_t *valroot)

Clone an existing MPID.

Parameters:
  • mpid -- Mount Point ID control block to clone

  • valroot -- value node getting this MPID

    • may be NULL

    • back pointer to the valroot not malloced pointer

Returns:

clone of mpid must be freed with sm_free_mpid

An example can be found in the u_toaster.c file. The first code snippet shows how the RPC operation retrieves the MPID used by the client.

uint8 id = 0;
ncx_sm_mpid_t *mpid = sm_get_mpid_from_msg(msg);
if (mpid != NULL) {
    res = get_id_from_mpid(mpid, &id);
    if (res != NO_ERR) {
        return res;
    }
}

toaster_state_t *state = &toaster_state[id];

The following code snippet shows how the old MPID is deleted in case it is set and the new MPID is cloned and saved if schema mount is not enabled then the MPID will be NULL and the toaster_state[0] state record will be used.

if (res == NO_ERR) {
    state->toaster_toasting = TRUE;
    sm_free_mpid(state->toaster_mpid);
    state->toaster_mpid = NULL;
    if (mpid) {
        state->toaster_mpid = sm_clone_mpid(mpid, NULL);
        // not checking for NULL here to avoid ifdef code
    }
} else {
    agt_record_error(
        scb,
        &msg->mhdr,
        NCX_LAYER_OPERATION,
        res,
        methnode,
        NCX_NT_NONE,
        NULL,
        NCX_NT_NONE,
        NULL);
}

Tutorial: Schema Mounted Virtual Toaster

The YumaPro Quickstart Guide has a tutorial to introduce the toaster.yang module. This tutorial assumes some familiarity with that introduction.

Summary:

  • Build and install the SIL for the Schema Mount root module

  • Configure and start the server for this example

  • Connect to the server with yangcli-pro: - Enable the Toaster for the Mounted Toaster - Start Making Toast for the Mounted Toaster - Receive the 'toastDone' Notification

Build the libvtoaster.so SIL Library

Note

  • There must be SIL code installed for the YANG module(s) containing the mount point.

  • All configuration ancestor nodes and the mount point itself must have EDIT callbacks installed for the mounted SIL callbacks to be invoked,

The make_sil_dir_pro script can be used to generate dummy SIL callbacks for the 'vtoaster.yang' module. This module is usually installed in the /usr/share/yumapro/modules/test/pass directory.

Generate the SIL Code Stubs

lab1$ make_sil_dir_pro vtoaster

modparms =
cmnparms = --indent=4 --module=vtoaster --unified=true

*** /usr/share/yumapro/modules/test/pass/vtoaster.yang
*** 0 Errors, 0 Warnings


*** /usr/share/yumapro/modules/test/pass/vtoaster.yang
*** 0 Errors, 0 Warnings


*** /usr/share/yumapro/modules/test/pass/vtoaster.yang
*** 0 Errors, 0 Warnings


*** /usr/share/yumapro/modules/test/pass/vtoaster.yang
*** 0 Errors, 0 Warnings

Run the following commands to get started:

  cd vtoaster
  make doc
  make opendoc

lab1$

Make and Install the SIL Code

Note that any warnings for unused parameters can be ignored. The empty code stubs should be implemented but that is not required to enable the Schema Mount SIL (or SIL-SA) code in the toaster module.

lab1$ cd vtoaster
lab1$ make DEBUG=1 WITH_SCHEMA_MOUNT=1

...

lab1$ sudo make DEBUG=1 WITH_SCHEMA_MOUNT=1 install

If the 'pthreads' library is used then add PTHREADS=1 to each command. Do not set PTHREADS unless the server is also build with this flag.

lab1$ cd vtoaster
lab1$ make DEBUG=1 WITH_SCHEMA_MOUNT=1 PTHREADS=1

...

lab1$ sudo make DEBUG=1 WITH_SCHEMA_MOUNT=1 PTHREADS=1 install

Configure and Run the Server

Create a Server Config File

The following netconfd-pro configuration file called toast.conf is assumed to be installed in the /home/lab1 directory.

netconfd-pro {
  log-level debug4
  access-control off
  no-watcher
  sm-config $HOME/toast.json
  module vtoaster
}

Create a Schema Mount Config File

The following netconfd-pro sm-config JSON file called toast.json is assumed to be installed in the HOME directory for the user. This configuration specifies that the 'toaster' module will be mounted under the mount point named 'toaster' in top-level module 'vtoaster'.

{
    "yumaworks-schema-mount:schema-mount" : {
       "sm-config" : [
           {
               "mp-module" : "vtoaster",
               "mp-label" : "toaster",
               "mp-config" : true,
               "mp-cli" : {
                   "module" : [ "toaster" ]
               }
           }
        ]
    }
}

Start the Server

The server can be started as follows for this example:

> netconfd-pro --config=/home/lab1/toast.conf

Connect to the Server with yangcli-pro

The yangcli-pro tool is used for this example.

> yangcli-pro --message-indent=1

Use the connect command to start a NETCONF session with the server.

If the schema-mount aware session is started successfully, there will be some mount point information printed when the session starts.

Example Schema Mount Session Report:

Schema Mount Point:
   modname: vtoaster
   label: toaster
   root: /vtoast:vtoasters/vtoast:vtoaster
   config: true
   active: true

   YANG Library:
   ietf-inet-types@2013-07-15
   ietf-yang-library@2016-06-21
   ietf-yang-types@2013-07-15
   toaster@2009-11-20

Create the Top Level and Mounted Config Data All at Once

yangcli-pro and netconfd-pro can create and modify top-level and mounted configuration data nodes in the same edit. The YANG data nodes created for mounted modules are no different than other child nodes within a container or list.

Example Using merge command

In this example the merge command is used to interactively fill in some configuration data. The /vtoasters/vtoaster mount point is created and the /toaster presence container node is created to enable the toaster. Virtual toasters '1' and '2' are created and enabled in the same edits.

lab1> merge /vtoasters -opt

Filling container /vtoasters:
Add optional list 'vtoaster'?
Enter Y for yes, N for no, or C to cancel: [default: Y]
lab1:merge>

Filling list /vtoasters/vtoaster:
Filling key leaf /vtoasters/vtoaster/id:
Enter uint8 value for leaf <id>
lab1:merge> 1

Add optional container 'toaster'?
Enter Y for yes, N for no, or C to cancel: [default: Y]
lab1:merge>

Filling container /vtoasters/vtoaster/toaster:
Add another entry for list 'vtoaster'?
Enter Y for yes, N for no, or C to cancel: [default: N]
lab1> y

Filling list /vtoasters/vtoaster:
Filling key leaf /vtoasters/vtoaster/id:
Enter uint8 value for leaf <id>
lab1> 2

Add optional container 'toaster'?
Enter Y for yes, N for no, or C to cancel: [default: Y]
lab1>

Filling container /vtoasters/vtoaster/toaster:
Add another entry for list 'vtoaster'?
Enter Y for yes, N for no, or C to cancel: [default: N]
lab1>

RPC OK Reply 4 for session 3 [default]:

lab1> commit

RPC OK Reply 5 for session 3 [default]:

lab1>

The XML message for the edit is shown below:

rpc message-id="4"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <edit-config>
  <target>
   <candidate/>
  </target>
  <default-operation>merge</default-operation>
  <test-option>set</test-option>
  <config>
   <vtoasters
    xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0"
    nc:operation="merge"
    xmlns="urn:yumaworks:params:xml:ns:vtoaster">
    <vtoaster>
     <id>1</id>
     <toaster xmlns="http://netconfcentral.org/ns/toaster"/>
    </vtoaster>
    <vtoaster>
     <id>2</id>
     <toaster xmlns="http://netconfcentral.org/ns/toaster"/>
    </vtoaster>
   </vtoasters>
  </config>
 </edit-config>
</rpc>

Example Using Configuration Mode Editing

The "config mode" editing can also create top-level and mounted data all at once.

Virtual toasters '3' and '4' are created and enabled in this example.

lab1> config term

lab1# vtoasters

lab1(vtoasters)# vtoaster 3 toaster

lab1(toaster)# exit

Applying 1 edit

lab1(vtoaster)# exit

lab1(vtoasters)# vtoaster 4 toaster

lab1(toaster)# apply

Applying 1 edit

lab1(toaster)# return

lab1>

Example <rpc> Request

The XML message for vtoaster '3' is shown below:

<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="11"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <edit-config>
  <target>
   <candidate/>
  </target>
  <default-operation>merge</default-operation>
  <test-option>set</test-option>
  <config>
   <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
    <vtoaster>
     <id>4</id>
     <toaster xmlns="http://netconfcentral.org/ns/toaster"/>
    </vtoaster>
   </vtoasters>
  </config>
 </edit-config>
</rpc>

Example <get-config>

The <get-config> RPC operation can be used to retrieve the running configuration. It will include the mount point configuration data as well.

> get-config source=running

RPC Data Reply 6 for session 3 [default]:

rpc-reply {
  data {
    vtoasters {
      vtoaster  1 {
        id 1
        toaster {
        }
      }
      vtoaster  2 {
        id 2
        toaster {
        }
      }
      vtoaster  3 {
        id 3
        toaster {
        }
      }
      vtoaster  4 {
        id 4
        toaster {
        }
      }
    }
  }
}

The XML response from the server is show below:

<?xml version="1.0" encoding="UTF-8"?>
<rpc-reply message-id="14"
 xmlns:ncx="http://netconfcentral.org/ns/yuma-ncx"
 ncx:last-modified="2023-02-25T01:09:49Z" ncx:etag="79065"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <data>
  <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
   <vtoaster>
    <id>1</id>
    <toaster xmlns="http://netconfcentral.org/ns/toaster"></toaster>
   </vtoaster>
   <vtoaster>
    <id>2</id>
    <toaster xmlns="http://netconfcentral.org/ns/toaster"></toaster>
   </vtoaster>
   <vtoaster>
    <id>3</id>
    <toaster xmlns="http://netconfcentral.org/ns/toaster"></toaster>
   </vtoaster>
   <vtoaster>
    <id>4</id>
    <toaster xmlns="http://netconfcentral.org/ns/toaster"></toaster>
   </vtoaster>
  </vtoasters>
 </data>
</rpc-reply>

Make Toast in the Mounted Toaster

After the /top/list1/mp1/toaster container is created, the 'make-toast' RPC operation can be used.

Note

RPC operations in mounted modules are invoked with the NETCONF <action> operation.

The action command in yangcli-pro can be used to invoke RPC operations in mounted modules.

In order to fill the optional input parameters, the --opt parameter is used

Create vtoaster 1:

lab1> action /vtoasters/vtoaster[id=1]/make-toast -opt

Filling optional mounted RPC /vtoasters/vtoaster/make-toast:
Enter uint32 value for leaf <toasterDoneness> [5]
lab1:action> 3

Enter identityref value for leaf <toasterToastType> [toast:wheat-bread]
lab1:action> frozen-waffle

RPC OK Reply 16 for session 3 [default]:

The XML message for this request:

<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="17"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <action xmlns="urn:ietf:params:xml:ns:yang:1">
  <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
   <vtoaster>
    <id>3</id>
    <make-toast xmlns="http://netconfcentral.org/ns/toaster">
     <toasterDoneness>5</toasterDoneness>
     <toasterToastType xmlns:toast="http://netconfcentral.org/ns/toaster">toast:wheat-bread</toasterToastType>
    </make-toast>
   </vtoaster>
  </vtoasters>
 </action>
</rpc>

Create vtoaster 3:

lab1> action /vtoasters/vtoaster[id=3]/make-toast -opt

Filling optional mounted RPC /vtoasters/vtoaster/make-toast:
Enter uint32 value for leaf <toasterDoneness> [5]
lab1:action>

Enter identityref value for leaf <toasterToastType> [toast:wheat-bread]
lab1:action>

RPC OK Reply 17 for session 3 [default]:

lab1>

The XML message for this request:

<?xml version="1.0" encoding="UTF-8"?>
<rpc message-id="17"
 xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
 <action xmlns="urn:ietf:params:xml:ns:yang:1">
  <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
   <vtoaster>
    <id>3</id>
    <make-toast xmlns="http://netconfcentral.org/ns/toaster">
     <toasterDoneness>5</toasterDoneness>
     <toasterToastType
      xmlns:toast="http://netconfcentral.org/ns/toaster">toast:wheat-bread</toasterToastType>
    </make-toast>
   </vtoaster>
  </vtoasters>
 </action>
</rpc>

Receive the ToastDone Notifications

After about 36 seconds the 'toastDone' notification should be received from the first vtoaster server. The yangcli-pro console output will show the entire notification message.

Incoming notification for session 1 [default]:
notification {
  eventTime 2023-02-25T01:27:56.852609Z
  vtoasters {
    vtoaster {
      id 1
      toastDone {
        toastStatus done
      }
    }
  }
}

lab1>

The XML message from the server is shown below:

<?xml version="1.0" encoding="UTF-8"?>
 <notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
  <eventTime>2023-02-25T01:27:56.852609Z</eventTime>
  <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
   <vtoaster>
    <id>1</id>
    <toastDone xmlns="http://netconfcentral.org/ns/toaster">
     <toastStatus>done</toastStatus>
    </toastDone>
   </vtoaster>
  </vtoasters>
 </notification>

After another 20 seconds the second notification for vtoaster '3' should arrive:

Incoming notification for session 1 [default]:
notification {
  eventTime 2023-02-25T01:28:36.012403Z
  vtoasters {
    vtoaster {
      id 3
      toastDone {
        toastStatus done
      }
    }
  }
}

The XML message from the server is shown below:

<?xml version="1.0" encoding="UTF-8"?>
<notification xmlns="urn:ietf:params:xml:ns:netconf:notification:1.0">
 <eventTime>2023-02-25T01:28:36.012403Z</eventTime>
 <vtoasters xmlns="urn:yumaworks:params:xml:ns:vtoaster">
  <vtoaster>
   <id>3</id>
   <toastDone xmlns="http://netconfcentral.org/ns/toaster">
    <toastStatus>done</toastStatus>
   </toastDone>
  </vtoaster>
 </vtoasters>
</notification>

Long-Term Usage Limitations

In addition to the Unsupported Features there are some usage restrictions within the netconfd-pro server.

  • A mount point node must be a configuration node. It cannot be an operational node (e.g. provided by a GET2 callback).

  • Only GET2 callbacks are allowed for operational data. Static val_value_t nodes and GET1 callbacks are not supported

  • Only EDIT2, EDIT3 callbacks should be used since EDIT1 callback mode is deprecated.

  • Only real object templates can be used to generate val_value_t data nodes for use in the server. Generic objects MUST NOT be used.

  • Mounted modules cannot be loaded or unloaded at run-time. Only the --sm-config CLI parameter can be used to load modules into a mount point.

  • There must be SIL callbacks installed for the mount point root and all its ancestors. If not, the SIL and SIL-SA callbacks for mounted objects can get skipped.

  • The mount point root object cannot contain any extra child nodes:

    • For a container, there must not be any children defined.

    • For a list, there must not be any children defined except the list keys.

    • There must not be any modules augmenting the mount point object.

  • TBD: it is possible that not all implementation limitations are known yet

Temporary Usage Limitations

  • Server Issues

    • Only one data node can be a mount point for a given label. If the same mount-point extension appears in multiple nodes in the same module then only the first node encountered will be supported as a mount point. The rest of the nodes using the same label will be ignored. Use a deviation-stmt to set all these ignored nodes to 'not-supported'.

    • Since this module is the one defined the labels, just pick a different label name for each desired mount point.

    • DELETE on node above the MP root can skip SIL callbacks for mounted objects

    • Each SIL-SA subsystem can support a single location.

      • Current implementation does not identity which mount point should be used for each module registered by each subsystem.