5.2. Creating and Writing (SNMP SET or user) to rows (and columns)

As mentioned in Section 5.1.5, there are three different write-states (RESERVE1, RESERVE2, ACTION) through which a write request to a column, or creation of a row, must pass before it is considered safe to change. If there is any failure during these write-states, it is automaticly free-ed and the request is discarded. If there are no problems, the new request will replace the original (if a column is being modified), or added (if the row did not exist beforehand).

There are also helper functions that are required for this to work, such as making a copy of the row (to keep the original in case the write is unsucessful), extracting the index tuple, creating the index tuple, free-ing the row and checking to see if a row can be safely deleted.

Most of these functions (and the helper ones) generated by the 'mib2c' tool have all of its basic functionality writen. However, some of them require special attention.

The functions are geared towards using SNMP SET and changing the columns of a row. As such their implementation is geared towards such goal, and at first it might be unclear how a sub-agent would create a row by itself. This will be explained in details in Section 5.2.6.2.

To have a better grasp of how the sub-agent would handle request, it is important to first explain some of the helper functions.

5.2.1. Row copy

netSnmpIETFWGTable_row_copy. As the name implies - this function purpose is to copy rows. The generated implementation takes care of copying all of the context structure records. Of interest might be the function snmp_clone_mem which copies the index tuple based on the length of the oid. The reason why a normal memcpy function is not used is due to the neccessity of error checking. If the function cannot determing the correct length of the index tuple (for example the index tuple length might be defined as zero) the copying of the row is stopped. Of course having rows with no index values should never have happened in the first place, but you never known.

5.2.2. Extracting index

The netSnmpIETFWGTable_extract_index is a very important function and needs tweaking to work. Its purpose is to extract the index tuple in an appropiate format for NetSNMP library to understand.

The function purpose is to extract from a linked list of indexes (passed in as netsnmp_index * hdr) its values and store them in the corresponding context structure.

The snippets of code generated by the 'mib2c' tool do most of the work. However the linking of the linked list entries must be done by the developer.

     netsnmp_variable_list var_nsIETFWGName;
...
     /**
      * Create variable to hold each component of the index
      */
    memset( & var_nsIETFWGName, 0x00, sizeof(var_nsIETFWGName) );
    var_nsIETFWGName.type = ASN_OCTET_STR;
    /** TODO: link this index to the next, or NULL for the last one */
 #ifdef TABLE_CONTAINER_TODO
    snmp_log(LOG_ERR, "netSnmpIETFWGTable_extract_index index 
                list not implemented        !\n" );
    return 0;
 #else
    var_nsIETFWGName.next_variable = & var_XX;
 #endif
        

The var_nsIETFWGName is the first index of the row. If there were more index values they would be defined as var_<name of columnar node>.

In this snippet of code the linked list of the index values is built. This linked list will be used by parse_oid_indexes to figure out where each index value is suppose to go.

Each of the var_<named values> (there would be more than just one in the example if the index tuple had more than one object defined) type is set to its type (ASN_OCTET_STR, ASN_INTEGER, etc - look in the net-snmp/library/asn1.h) and linked to the next index value. The last index value is set to NULL.

For example, if this table had two extra index values: an enumerated integer value (nsIETFWGProgress) and TruthValue (nsIETFWGIsWorking) with the following ASN.1 definition:

nsIETFWGProgress OBJECT-TYPE
    SYNTAX      INTEGER {
                        undefined(0),
                        proposed(1),
                        debated(2),
                        rewritting(3),
                        draft(4),
                        standard(50)                                    
                }       
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Progress of a work-group"
    ::= { netSnmpIETFWGEntry 4 }

nsIETFWGIsWorking OBJECT-TYPE
    SYNTAX      TruthValue
    MAX-ACCESS  read-only
    STATUS      current
    DESCRIPTION
        "Is the work group still working?"
    ::= { netSnmpIETFWGEntry 5 }

netSnmpIETFWGEntry would have these two objects as extra index nodes:

       INDEX   { nsIETFWGName, nsIETFWGProgress, nsIETFWGIsWorking }

These two extra ASN.1 entries would add two extra variables in the context structure:

        /** INTEGER = ASN_INTEGER */
    long nsIETFWGProgress;

        /** TruthValue = ASN_INTEGER */
    long nsIETFWGIsWorking;

As such, the _extract_index code snippet would look like:

    netsnmp_variable_list var_nsIETFWGName;
    netsnmp_variable_list var_nsIETFWGProgress;
    netsnmp_variable_list var_nsIETFWGIsWorking;

    memset( & var_nsIETFWGName, 0x00, sizeof(var_nsIETFWGName) );
    var_nsIETFWGName.type = ASN_OCTET_STR;
    var_nsIETFWGName.next_variable = & var_nsIETFWGProgress;

    memset( & var_nsIETFWGProgress, 0x00, sizeof(var_nsIETFWGProgress) );
    var_nsIETFWGProgress.type = ASN_INTEGER;
    var_nsIETFWGProgress.next_variable = & var_nsIETFWGIsWorking;

    memset( & var_nsIETFWGIsWorking, 0x00, sizeof(var_nsIETFWGIsWorking));
    var_nsIETFWGIsWorking.type = ASN_INTEGER;
    var_nsIETFWGIsWorking.next_variable = NULL;

The parse_oid_indexes parses the linked list of index value against the hdr, checks to make sure it is right type, and correct length. If everything is correct, each item of the linked list is populated with the index value taken from the hdr.

However, that does not fill the values of context structure. That is the job of the next part of the code segment in which the return code of parse_oid_indexes is checked, and if found to be OK, the values from linked list are copied to the corresponding context structure entries.

    err = parse_oid_indexes( hdr->oids, hdr->len, & var_nsIETFWGName );
    if (err == SNMP_ERR_NOERROR) {
       /*
        * copy components into the context structure
        */
              /** skipping external index nsIETFWGName */
                if(var_nsIETFWGName.val_len > sizeof(ctx->nsIETFWGName))
                   err = -1;
                else
                    memcpy( ctx->nsIETFWGName, 
                           var_nsIETFWGName.val.string, 
                           var_nsIETFWGName.val_len );
                ctx->nsIETFWGName_len = var_nsIETFWGName.val_len;
    } 

Lastly the linked list is cleaned up (during the parsing it might have allocated memory) using:

     snmp_reset_var_buffers( & var_nsIETFWGName );
Note

If you add more index values and change the order in which they appear, make sure that it is always the first index tuple, defined as one of var_<name of columnar node> is being free-ed using snmp_reset_var_buffers.

5.2.3. Row deletion checking

netSnmpIETFWGTable_can_delete does a small check on the passed context structure to see if it can be deleted.

If there are any specific conditions under which a row can not be deleted these should be implemented here.

By default the function returns a positive number, which implies that the row can be safely deleted.

Returning zero means that the row cannot be deleted.

5.2.4. Duplicating rows

netSnmpIETFWGTable_duplicate_row is pretty self-explanatory. Duplicate a row.

5.2.5. Deleting rows

netSnmpIETFWGTable_delete_row purpose is to delete a row. This is function that frees the index tuple, and any other memory that the user might have allocated using

     void * data;

defined in the context structure in the header file.

This function is only used when the NetSNMP library is trying to write to a row and finds out that something is wrong. Then it deletes the temporary row (a copy of the original).

To delete a row from the sub-agent, you need to use the CONTAINER_REMOVE macro. Look in Section 5.2.6.3 for more information.

5.2.6. Creating rows

The netSnmpIETFWGTable_create_row purpose is to create a newly allocated context structure.

netSnmpIETFWGTable_context *
netSnmpIETFWGTable_create_row( netsnmp_index* hdr)
{
    netSnmpIETFWGTable_context * ctx =
        SNMP_MALLOC_TYPEDEF(netSnmpIETFWGTable_context);
    if(!ctx)
        return NULL;
        
    /*
     * TODO: check indexes, if necessary.
     */
    if(netSnmpIETFWGTable_extract_index( ctx, hdr )) {
        free(ctx->index.oids);
        free(ctx);
        return NULL;
    }

    /* netsnmp_mutex_init(ctx->lock);
       netsnmp_mutex_lock(ctx->lock); */

    /*
     * TODO: initialize any default values here. This is also
     * first place you really should allocate any memory for
     * yourself to use.  If you allocated memory earlier,
     * make sure you free it for earlier error cases!
     */
    ctx->nsIETFWGChair1_len = 0;
    ctx->nsIETFWGChair2_len = 0;

    return ctx;
}

The passed in argument netsnmp_index* hdr defines the index of the row. This hdr value is checked using netSnmpIETFWGTable_extract_index routine which extracts the index values from hdr and populates the correct entries in the context structure. If this call fails, the newly allocated context structure is free-ed and the function returns a NULL.

Note

In case you do not know how netSnmpIETFWGTable_extract_index knows which entries to populate, refer to Section 5.2.2, for details.

Note

The netsnmp_mutex_init(ctx->lock); purpose is to create a locking mutex mechanism in case your application is multi-threaded.

Otherwise the context is filled with default values - specified by the developer in the last lines of this routine.

5.2.6.1. What calls netSnmpIETFWGTable_create_row>

Looking back at Section 5.1.5, which described part of the initialization process for a table, the netSnmpIETFWGTable_create_row is initialized in call-back mechansim:

     #ifdef netSnmpIETFWGTable_ROW_CREATION
         cb.create_row = (UserRowMethod*)netSnmpIETFWGTable_create_row;
     #endif

The NetSNMP library uses this information to call the create_row function whenever a SET request is issued against a non-existing row. The NetSNMP library first searches through the rows - if it cannot find a row with the matching index value, it calls netSnmpIETFWGTable_create_row. If this call returns a NULL value the SET request is dropped. Otherwise the next call the NetSNMP library makes is the netSnmpIETFWGTable_set_reserve1 routine, explained later in this chapter.

The create_row function is also the vehicle by which the sub-agent internally can create fully populated rows.

5.2.6.2. Developer row creation

The source code has a perfect spot for user creation - it is right after the call to initialize_table_netSnmpIETFWGTable.

    void
    init_netSnmpIETFWGTable(void)
    {
         initialize_table_netSnmpIETFWGTable();
    
         /*
          * TODO: perform any startup stuff here
          */
    }

After the initialize call, the table is ready to populated with rows. The rows are the context structures, explained in the previous sections.

Consult Section 4.1.1 for more in depth explanation of the context structure.

The big question is what magic will make NetSNMP library aware of the container structures? By the usage of CONTAINER macros. Look in net-snmp/library/container.h. In it, there are a couple of macro calls:

   /*
     * useful macros
     */
#define CONTAINER_FIRST(x)          (x)->find_next(x,NULL)
#define CONTAINER_FIND(x,k)         (x)->find(x,k)
#define CONTAINER_NEXT(x,k)         (x)->find_next(x,k)
#define CONTAINER_GET_SUBSET(x,k)   (x)->get_subset(x,k)
#define CONTAINER_SIZE(x)           (x)->get_size(x)
#define CONTAINER_ITERATOR(x)       (x)->get_iterator(x)
#define CONTAINER_COMPARE(x,l,r)    (x)->compare(l,r)
#define CONTAINER_FOR_EACH(x,f,c)   (x)->for_each(x,f,c)

These macros provide a wrapper around the row-data (context structure) and make its manipulation possible.

5.2.6.2.1. Example of CONTAINER usage

A good comprehensive example of how to use CONTAINER_ macros is available in NetSNMP source tarball - in the snmplib/test_binary_array.c , or the following example in this tutorial (these lines of code were inserted after initialize_table_netSnmpIETFWGTable().

Note

It is assumed that create_row function is fully implemented for this example to work properly.

static void
print_string(netsnmp_index *i, void *v)
{
    int a;
    printf("item %p = [",i);
    for (a = 1; a <= i->oids[0]; a++)
        printf("%c", (char) i->oids[a]);
    printf("]\n");
}

/************************************************************
 * Initializes the netSnmpIETFWGTable module
 */
void
init_netSnmpIETFWGTable(void)
{
   netsnmp_index index;
   oid index_oid[MAX_OID_LEN];
   char *index_char[] = {"hickory","joe","hickory",
                         "bob","new orleans","help"};
   int i,j;
   netSnmpIETFWGTable_context *ctx;

   initialize_table_netSnmpIETFWGTable();

   for (i = 0; i< 6; i++) {
     /*
       First value of an index that is ASN_OCTET_STR is 
       the length of the string.
     */
        index_oid[0] = strlen(index_char[i]);
        /* The rest is the data copied. */
        for (j = 0; j < index_oid[0];j++) {
                index_oid[j+1] = *(index_char[i]+j);    

        }
        index.oids = (oid *) & index_oid;
        index.len = index_oid[0]+1;
        ctx = NULL;
        /* Search for it first. */
        ctx = CONTAINER_FIND (cb.container, & index);
        if (!ctx) {
          /* No dice. We add the new row */
                ctx = netSnmpIETFWGTable_create_row( & index);
                printf("inserting %s\n", ctx->       nsIETFWGName);
                CONTAINER_INSERT (cb.container, ctx);
        }

   } 
   /*
    Since we are done adding the rows, let us display them for the fun.
    The easy way:
   */

   CONTAINER_FOR_EACH(cb.container, print_string, NULL);

   /*     
   We do not like 'joe', so we remove him.
   */
   index_oid[0] = 3;
   index_oid[1] = 'j'; index_oid[2] = 'o'; index_oid[3] = 'e';
   index.oids = (oid *) & index_oid;
   index.len = 4;

   ctx = CONTAINER_FIND(cb.container, & index);
   if (ctx) {
        CONTAINER_REMOVE( cb.container, & index);
        netSnmpIETFWGTable_delete_row ( ctx );
        printf("Removed joe\n");
   }
   /*
     Print the hard way:
   */
   
   ctx = CONTAINER_FIRST(cb.container);
   printf("Find first = %p %s\n",ctx, ctx->nsIETFWGName);
    while( ctx ) {
        ctx = CONTAINER_NEXT(cb.container,ctx);
        if(ctx)
            printf("Find next = %p %s\n",ctx, ctx->nsIETFWGName);
        else
            printf("Find next = %p\n",ctx);
    }

  
}

The output of this daemon should look like this:

inserting hickory
inserting joe
inserting bob
inserting new orleans
inserting help
item 0x4036d008 = [bob]
item 0x4033c008 = [joe]
item 0x4030b008 = [hickory]
item 0x4039e008 = [new orleans]
item 0x403cf008 = [help]
Removed joe
Find first = 0x4036d008 bob
Find next = 0x403cf008 help
Find next = 0x4030b008 hickory
Find next = 0x4039e008 new orleans
Find next = (nil)

5.2.6.2.2. Sorted

You might wonder why the rows are alphabethicly sorted? The reason is that the NetSNMP library uses its own sorting method when adding/deleting rows. It is set by default to be the function netsnmp_compare_netsnmp_index which has the exact same function prototype as the static int netSnmpIETFWGTable_cmp( const void *lhs, const void *rhs );. If you would like to use your own sorting method, add in initialize_table_netSnmpIETFWGTable function the following line:

           cb.container->compare = netSnmpIETFWGTable_cmp;

And make sure that your compare function works properly. Consult Section 5.1.8 and Section 5.1.4 for more details.

5.2.6.3. Developer row deletion.

Removing rows requires four steps. You have know which row you want (by the index value), find it, remove from the container, and then finally free it.

   /*     
   We do not like 'joe', so we remove him.
   */
   index_oid[0] = 3;
   index_oid[1] = 'j'; index_oid[2] = 'o'; index_oid[3] = 'e';
   index.oids = (oid *) & index_oid;
   index.len = 4;

   ctx = CONTAINER_FIND(cb.container, & index);
   if (ctx) {
        CONTAINER_REMOVE( cb.container, & index);
        netSnmpIETFWGTable_delete_row ( ctx );
        printf("Removed joe\n");
   }

5.2.7. Writing to a row

There are two ways to write to a row. From the perspective of a SNMP SET command and sub-agent (developer).

5.2.7.1. Developer writing to a row

The process of writing to a row from a sub-agent perspective (developer) is simplistic. It requires the task of retrieving the row and modifying it. No need to re-insert it using CONTAINER_INSERT, since it is already in there.

   char chair* = "John Block";
   /*
    * Modify 'bob'
    */
   index_oid[0] = 3;
   index_oid[1] = 'b'; index_oid[2] = 'o'; index_oid[3] = 'b';
   index.oids = (oid *) & index_oid;
   index.len = 4;
   ctx = CONTAINER_FIND(cb.container, & index);
   if (ctx) {
        /* Modify the context to our content. */
        ctx->nsIETFWGChair1_len = strlen(chair);
        memcpy(ctx->nsIETFWGChair1, chair, ctx->nsIETFWGChair1_len);
   }

5.2.7.2. SNMP SET writing to a row

This process is more complex due to the neccessity of checking that the SNMP SET request is the correct type, length, and other checks that the developer might deem neccesary.

The process by which the NetSNMP library uses to decide if the data is OK is by a state machine. If the SET request passes succesfully through the RESERVE1, RESERVE2, and ACTION phase it is committed to memory.

The following picture, borrowed from NetSNMP webpage, demonstrates these steps. A more detailed explanation of what happens during these steps is explained in this rstory's NET-SNMP Developers Frequently Asked Questions Page: Baby Steps Flow.

5.2.8. RESERVE1 function

The netSnmpIETFWGTable_set_reserve1 job is to make sure that the SET request is of right type.

 for( current = rg->list; current; current = current->next ) {

        var = current->ri->requestvb;
        rc = SNMP_ERR_NOERROR;

        switch(current->tri->colnum) {

        case COLUMN_NSIETFWGCHAIR1:
            /** OCTETSTR = ASN_OCTET_STR */
            rc = netsnmp_check_vb_type_and_size(var, ASN_OCTET_STR,
                                    sizeof(row_ctx->nsIETFWGChair1));
        break;

The for loop goes through all of the SNMP SET requests. The netsnmp_request_group *rg keeps a list of aggregated SNMP SET request for this particular table.

Note

This can mean that this function is called with more than one SNMP SET request for different columns.

Note

This list of SNMP SET request can also be for non-existent rows, because the index values do not match what the NetSNMP library has in memory. For rows that do not exist in the container (as in, they have not been inserted using CONTAINER_INSERT), the NetSNMP creates a context structure using the netSnmpIETFWGTable_create_row. For those that do exist, it grabs them from the container.

The netsnmp_check_vb_type_and_size checks the type of the SNMP SET request and also the size of the payload.

If checking process fails, the rc is set to an appropiate error code (consult net-snmp/library/snmp.h for the list) and netsnmp_set_mode_request_error is notified. This will result in removal of this SNMP SET request and the end-user will be notified of the appropiate error code.

Note

If the end-user is using SNMP v1, only a selective set of error codes is available. This might give the user a different error code than what the developer had set.

5.2.9. RESERVE2 function

The second stage is checking for appropiate values of the SNMP SET request. This is where the developer checks for the correct length and values of the columnar nodes.

This check is necessary for enumerated integers - it is important to check for the right enumeration values. If you refer back to Section 5.2.2, you will notice the extra defined ASN.1 columnar node called nsIETFWGProgress. This object defines six enumerations: undefined(0), proposed(1), debated(2), rewritting(3), draft(4) and standard(50). The check for the proper enumerations can be carried out in this function, such as:

        case COLUMN_NSIETFWGPROGRESS:
            if (((*var->val.integer < 0) ||
               (*var->val.integer > 4)) && (*var->val.integer != 50))
            {
              rc = SNMP_ERR_BADVALUE;
            }
          break;

For string variables, it important to check the length of the string - to make sure it is not more (or less) to what is defined in the MIB.

nsIETFWGChair1 OBJECT-TYPE
    SYNTAX      OCTET STRING
    MAX-ACCESS  read-create
    STATUS      current
    DESCRIPTION
        "One of the names of the chairs for the IETF working group."
    ::= { netSnmpIETFWGEntry 2 }
Note

And our check for strings of length more than 255. In truth, this check might be inappropriate because an OCTET STRING can have a length of 65536. However most SNMP implementations cannot carry such large payloads.

        case COLUMN_NSIETFWGCHAIR2:
            /** OCTETSTR = ASN_OCTET_STR */
          if (var->val_len > 255)
                rc = SNMP_ERR_WRONGLENGTH;
        break;

5.2.10. ACTION

The netSnmpIETFWGTable_set_action is the function where the new value is writen in the appropiate context structure and where the developer would perform the write to his/her datastore.

The changes are being writen to row_ctx. A copy of the original row is in undo_ctx. If this function calls netsnmp_set_mode_request_error the newly modified row is discarded and the user is notified of the error state.

It is in this function that the developer can decide if the row has to be removed or created. For deleting, the variable row_deleted in the netsnmp_request_group has to be set:

       rg->row_deleted = 1;

For creating:

       rg->row_created = 1;

5.2.11. COMMIT function

This routine is used to commit the changes to the row. The intent of the ACTION/COMMIT division is that all of the fallible code should be done in the ACTION phase, so that it can be backed out if necessary.

5.2.12. FREE and UNDO function

This explanation is taken from the generated C code.

5.2.12.1. FREE function

If either of the RESERVE calls fail, the write routines are called again with the FREE action, to release any resources that have been allocated. The agent will then return a failure response to the requesting application.

AFTER calling this routine, the agent will delete undo_info.

5.2.12.2. UNDO function

If the ACTION phase does fail (for example due to an apparently valid, but unacceptable value, or an unforeseen problem), then the list of write routines are called again, with the UNDO action. This requires the routine to reset the value that was changed to its previous value (assuming it was actually changed), and then to release any resources that had been allocated. As with the FREE phase, the agent will then return an indication of the error to the requesting application.

BEFORE calling this routine, the agent will update the container (remove any newly inserted row, re-insert any removed row).