Non-table-based modules

Having looked at the general structure of a module implementation, it's now time to look at this in more detail. We'll start with the simplest style of module - a collection of independent variables. This could easily be implemented as a series of completely separate modules - the main reason for combining them is to avoid the proliferation of multiple versions of very similar code.

Recall that the variable handling routine needs to cover two distinct purposes - validation of the request, and provision of the answer. In this style of module, these are handled separately. Once again, mib2c does much of the donkey work, generating the whole of the request validation code (so the description of this section can be skipped if desired), and even providing a skeleton for returning the data. This latter still requires some input from the programmer, to actually return the correct results (rather than dummy values).

Request Validation

This is done using a standard utility function header_generic. The parameters for this are exactly the same as for the main routine, and are simply passed through directly. It returns an integer result, as a flag to indicate whether the validation succeeded or not.
If the validation fails, then the main routine should return immediately, leaving the parameters untouched, and indicate the failure by returning a NULL value. Thus the initial code fragment of a scalar-variable style implementation will typically look like:
    u_char  *
    var_system(vp, name, length, exact, var_len, write_method)
    {
	if (header_generic(vp, name, length, exact, var_len, write_method)
						== MATCH_FAILED )
	    return NULL;

	[ etc, etc, etc ]
    }
Although the utility function can be used as a "black box", it's worth looking more closely at exactly what it does (since the table-handling modules will need to do something fairly similar). It has two (or possibly three) separate functions:
In order to actually validate the request, the header routine first needs to construct the OID under consideration, in order to compare it with that originally asked for. The driving code has already combined the OID prefix (constant throughout the module) with the entry-specific suffix, before calling the main variable handler. This is available via the name field of the parameter vp. For a scalar variable, completing the OID is therefore simply a matter of appending the instance identifier 0 to this. The full OID is built up in a local oid array newname defined for this purpose.
This gives the following code fragment:
    int
    header_generic(vp, name, length, exact, var_len, write_method)
    {
	oid newname[MAX_NAME_LEN];

	memcpy((char *)newname, (char *)vp->name,
			(int)vp->namelen * sizeof(oid));
	newname[ vp->namelen ] = 0;

		:
    }

Having formed the OID, this can then be compared against the variable specified in the original request, which is available as the name parameter. This comparison is done using the snmp_oid_compare function, which takes the two OIDs (together with their respective lengths), and returns -1, 0 or 1 depending on whether the first OID precedes, matches or follows the second.

In the case of an 'exact' match (i.e. a GET/SET/etc), then the request is only valid if the two OIDs are identical (snmp_oid_compare returns 0). In the case of a GETNEXT (or GETBULK) request, it's valid if the OID being considered comes after that of the original request (snmp_oid_compare returns -1).

This gives the code fragment

	result = snmp_oid_compare(name, *length, newname, (int)vp->namelen + 1);
			// +1 because of the extra instance sub-identifier
	if ((exact && (result != 0))		// GET match fails
		|| (!exact && (result >= 0)))	// GETNEXT match fails
	    return(MATCH_FAILED);
Note that in this case, we're only interested in the single variable indicated by the vp parameter. The fact that this module may well implement other variables as well is ignored. The 'lexically next' requirement of the GETNEXT request is handled by working through the variable entries in order until one matches. And yes, this is not the most efficient implementation possible!
Note that in releases prior to 3.6, the snmp_oid_compare function was called simply compare.

Finally, having determined that the request is valid, this routine must update the name and length parameters to return the OID being processed. It also sets default values for the other two return parameters.

	memcpy( (char *)name,(char *)newname,
		((int)vp->namelen + 1) * sizeof(oid));
	*length = vp->namelen + 1;
	*write_method = 0;		// Non-writeable
	*var_len = sizeof(long);	// default to integer results
	return(MATCH_SUCCEEDED);
These three code fragments combine to form the full header_generic code which can be seen in the file util_funcs.c

Note: This validation used to be done using a separate function for each module (conventionally called header_{name}), and many modules may still be coded in this style. The code for these are to all intents and purposes identical to the header_generic routine described above.

Data Retrieval

The other main job of the request handling routine is to retrieve any necessary data, and return the appropriate answer to the original request. This must be done even if mib2c is being used to generate the framework of the implementation. As has been indicated earlier, the different cases are handled using a switch statement, with the Magic Number field of the vp parameter being used to distinguish between them.
The data necessary for answering the request can be retrieved for each variable individually in the relevant case statement (as is the case with the system group), or using a common block of data before processing the switch (as is done for the ICMP group, among others).

With many of the modules implemented so far, this data is read from a kernel structure. This can be done using the auto_nlist routine already mentioned, providing a variable in which to store the results and an indication of its size (see the !HAVE_SYS_TCPIPSTATS_H case of the ICMP group for an example). Alternatively, there may be ioctl calls on suitable devices, specific system calls, or special files that can be read to provide the necessary information.

If the available data provides the requested value immediately, then the individual branch becomes a simple assignment to the appropriate static return variable - either one of the global static variables (e.g long_return) or the local equivalents (such as generated bymib2c). Otherwise, the requested value may need to be calculated by combining two or more items of data (e.g. IPINHDRERRORS in mibII/ip.c) or by applying a mapping or other calculation involving available information (e.g. IPFORWARDING from the same group).

In each of these cases, the routine should return a pointer to the result value, casting this to the pseudo-generic (u_char *)


So much for the scalar case. The next part looks at how to handle simple tables.


Copyright 1999 - D.T.Shield. Not to be distributed without the explicit permission of the author.