-
Notifications
You must be signed in to change notification settings - Fork 1
/
AGENT.txt
1171 lines (965 loc) · 55.4 KB
/
AGENT.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
Note, this is based on the text from a web page, which can be found in
the documentation section of the http://www.net-snmp.org web page.
Extending the UCD-SNMP agent
============================
This document describes the procedure for writing code to extend
the functionality of the v4 UCD-SNMP network management agent.
Modules written using this procedure should also work with the v5
Net-SNMP agent, though such modules would not take advantage of the
new handler-based helper mechanism. See the on-line documentation
for more information and examples of the newer approach.
We would be very interested in comment and feedback about how useful
(or otherwise) you find this description, and ways in which it could
be improved.
The information is designed to be read in order - the structure being:
1. Overview & Introduction
2. MIB files, and how they relate to the agent implementation
3. Header files
4. The basic structure of module implementation code
5. The details of non-table based implementations
6. The details of simple table based implementations
7. The details of more general table based implementations
8. How to implement SET-able variables
While the document is intended to be generally self-contained,
it does occasionally refer to code files shipped with the main UCD
distribution (in particular the example module), and it may prove
useful to have these files available for reference.
1. How to write a Mib module
============================
Introduction
------------
The design of the UCD SNMP agent has always been shaped by the desire to be
able to extend its functionality by adding new modules. One of the earliest
developments from the underlying CMU code base was the ability to call
external scripts, and this is probably the simplest method of extending the
agent.
However, there are circumstances where such an approach is felt to be
inappropriate - perhaps from considerations of speed, access to the
necessary data, reliability or elegance. In such cases, the obvious solution
is to provide C code that can be compiled into the agent itself to implement
the desired module. Many of the more recent developments in the code
structure have been intended to ease this process. In particular, one of the
more recent additions to the suite is the tool mib2c. This is designed to
take a portion of the MIB tree (as defined by a MIB file) and generate the
code skeleton necessary to implement this. This document will cover the use
mib2c, as well as describing the requirements and functionality of the code
in more detail.
In order to implement a new MIB module, three files are necessary, and these
will be considered in turn. Note that, by the very nature of the task, this
document cannot cover the details of precisely how to obtain the necessary
information from the operating system or application. Instead, it describes
the code framework that is needed, freeing the implementer from needing to
understand the detailed internals of the agent, and allowing them to
concentrate on the particular problem in hand.
It may prove useful to examine some of the existing module implementations
and examples in the light of this description, and suitable examples will be
referred to at the appropriate points. However, it should be remembered that
the UCD agent seeks to support a wide variety of systems, often with
dramatically differing implementations and interfaces, and this is reflected
in the complexity of the code. Also, the agent has developed gradually over
the years, and there is often some measure of duplication or redundancy as a
result.
As the FAQ states, the official slogan of the UCD-SNMP developers is
The current implementation is non-obvious and may need to be
improved.
This document describes the ideal, straightforward cases - real life is
rarely so simple, and the example modules may prove easier to follow at a
first reading.
It is also advisable to have a compiled and installed implementation
available before starting to extend the agent. This will make debugging and
testing the agent much easier.
A note regarding terminology - the word "module" is widely used throughout
this document, with a number of different meanings.
* support for a new MIB,
i.e. the whole of the functionality that is required. This is usually
termed a MIB module;
* a self-contained subset of this, implemented as a single unit.
This is usually termed an implementation module (or simply "a module");
* the combination of such subsets, usually termed a module group.
Note that the first and third of these are often synonymous - the
difference being that a MIB module refers to the view from outside the
agent, regarding this as a seamless whole and hiding the internal
implementation. A "module group" is used where the internal structure is of
more relevance, and recognises the fact that the functionality may be
provided by a number of co-operating implementation modules.
Anyway, enough waffle - on with the details: The three files needed are
* a MIB definition file;
* a C header file;
* a C implementation file.
The next part looks at the MIB definition file, and how this impacts on the
agent implementation.
2. The MIB File
===============
The first file needed is the MIB file that defines the MIB module to be
implemented.
Strictly speaking, this is not absolutely necessary, as the agent itself
does not make any direct use of the MIB definitions. However, it is
advisable to start with this for three reasons:
* It provides an initial specification for what is to be implemented.
Code development is always easier if you know what you are meant to be
writing!
* If the new MIB file is read in with the other MIB files,
this lets the applications provided with the suite be used to test the
new agent, and report (hopefully meaningful) symbolic OIDs and values,
rather than the bare numeric forms.
(N.B: Remember to tell the application to load the new MIB. See the
relevant question in the FAQ)
* The tool mib2c uses this description to produce the two code files.
This is by far the easiest way to develop a new module.
(Note that the v5 version of mib2c is generally similar, but does
not correspond exactly to the v4 version described here)
If the intention is to implement a 'standard' MIB module, or a
vendor-specific one, then the construction of this file will have already
been done for you. If the intention is to provide a totally new, private
module, then you will need to write this yourself, in addition to the agent
code files.
A description of MIB file format and syntax is beyond the scope of this
document, and most books on SNMP management should provide some information
on this subject. One book which concentrates on this is
Understanding SNMP MIBS
(Perkins & McGinnis, Prentice Hall, ISBN 0-13-437708-7).
This blatant plug is wholly unrelated to the fact that David Perkins is an
active member of the development group, and is regarded as our resident
"protocol guru and policeman". (In fact, this book concentrates on MIB
files in rather more detail than is appropriate in more general SNMP works).
Information on other books covering SNMP and Network Management more generally
is available on the SimpleWeb site (among other places).
See the FAQ for more details.
Assigned OID numbers
--------------------
One word of advice - even if you are developing a totally private MIB
module, you will still need to position this somewhere within the overall
MIB tree. Please do NOT simply choose a location "at random". Any such is
likely to have either been assigned to some other organisation, or may be so
assigned some time in the future. However much you may regard your project
as a totally internal affair, such projects have a tendency to exceed their
expected scope, both in terms of lifetime and distribution (not to mention
the potential OID clash if you subsequently need to use elements from the
legitimate owner's tree).
It is simple and cheap (i.e. free!) to obtain your own official segment of
the MIB tree (see http://www.iana.org for an application form), and having
done so, you then have complete global authority over it. If you have
problems with this, it's worth contacting the development team (email:
net-snmp-coders@lists.sourceforge.net) for advice. Please do think to the
future, and be a good Net citizen by using a legitimately assigned OID as
the root of your new MIB.
MIB division
------------
The next point to consider, whether writing by hand or using mib2c,
implementing an existing MIB, or writing a new one, is whether and how to
divide up the MIB tree. This is a purely internal implementation decision,
and will not be visible to management applications querying the agent. A
sensible choice of partitioning will result in a simpler, clearer
implementation, which should ease both the initial development and
subsequent maintenance of the module.
Unfortunately, this choice is one of the module-specific decisions, so must
be made on a case-by-case basis. For a simple, self-contained module, it may
well be reasonable to implement the module as a single block (examples
include the SNMP statistics subtree RFC 1907 or the TCP subtree RFC 2011).
More complex and diverse modules (such as the Host Resources MIB - RFC 1514)
are more naturally considered as a number of individual sub-modules.
Some guidelines to bear in mind when deciding on this division:
* A MIB sub-tree consisting purely of scalar objects with a common
OID prefix would normally be handled in a single implementation module;
* Separate scalar subtrees would normally be in different implementation
modules;
* A table can either be handled within the same implementation module
as related scalar objects in the same subtree, or in a separate
implementation module;
* Variables that rely on the same underlying data structure to retrieve
their values, should probably be in the same implementation module (and
conversely, (though less so) those that don't, shouldn't).
As an initial rule of thumb, a good initial division is likely to be
obtained by treating each table and each scalar sub-tree separately. This
can be seen in the current agent, where most of the MIB-II modules (RFC
1213) are implemented in separate files (see the files under mibgroup/mibII).
Note that many of these combine scalar and table handling in the same file,
though they are implemented using separate routines.
This is also the approach used by mib2c, which constructs a single pair of
code files, but uses a separate routine for each table (and another for all
the scalar variables).
Ultimately, the final consideration (concerning the underlying data) is
the most important, and should guide the basic division. For example, the
Host Resources Running Software and Running Software Performance modules,
while separate in the MIB tree, use the same underlying kernel data and so
are implemented together.
MIB name
--------
The final requirement at this stage is to choose a name for each
implementation module. This should be reasonably short, meaningful, unique
and unlikely to clash with other (existing or future) modules. Mib2c uses
the label of the root node of the MIB sub-tree as this name, and this is a
reasonable choice in most cases.
Recent changes to the agent code organisation have introduced the idea of
module groups of related implementation modules. This is used, for example,
to identify the constituent modules of a 'split' MIB (such as the Host
Resources MIB), or those relating to a particular organisation (such as
UCD).
As with the division, this naming and grouping is a purely internal matter,
and is really only visible when configuring and compiling the agent.
So much for the MIB file. The next part considers the C header file.
3. The C code header file
=========================
If the MIB file is the definition of the module for external network
management applications (where applications includes network management
personnel!), then the header file has traditionally served effectively the
same purpose for the agent itself.
Recent changes to the recommended code structure has resulted in the header
file becoming increasingly simpler. It now simply contains definitions of the
publically visible routines, and can be generated completely by mib2c.
Function prototypes
-------------------
For those interested in the details of this file (for example, if coding a
module by hand), then the details of these definitions are as follows. Every
header file will have the following two function prototype definitions
extern void init_example (void);
extern FindVarMethod var_example;
If the module includes any tables, or other collections of variables that
are implemented in separate routines, then this second definition will be
repeated for each of these.
In addition, if any of the variables can be SET (and it is intended to
implement them as such), there will be a function prototype definitions for
each of these, of the form:
extern WriteMethod write_varName;
These prototypes are in fact typedef'ed in <agent/snmp_vars.h>.
Module dependencies
-------------------
This header file is also used to inform the compilation system of any
dependancies between this module and any others. There is one utility module
which is required by almost every module, and this is included using the
directive
config_require( util_funcs )
(which is produced automatically by mib2c). This same syntax can be used to
trigger the inclusion of other related modules. An example of this can be
seen in mibII/route_write.h which relies on the mibII/ip module, thus:
config_require( mibII/ip )
One use of this directive is to define a module group, by supplying a header
file consisting exclusively of such config_require directives. It can then
be included or excluded from the agent very simply. Examples of this can be
seen in mibgroup/mibII.h or mibgroup/host.h, which list the consituent
sub-modules of the MIB-II and Host Resources MIBs respectively.
MIB file information
--------------------
Most of the information in this file is (understandably) aimed at the network
management agent itself. However, there is one common header file directive
that is actually intended to affect the utility commands that are included
within the full distribution:
config_add_mib( HOST-RESOURCES-MIB )
This is used to add the MIB file being implemented to the default list of
MIBs loaded by such commands. This means that querying the agent will return
informative names and values, rather than the raw numeric forms that SNMP
actually works with. Of course, it is always possible for the utilities
to specify that this MIB should be loaded anyway. But specifying this file
within the module header file is a useful hint that a particular MIB should
be loaded, without needing to ask for it explicitly.
Note that this will only affect the binaries compiled as part of the same
configuration run. It will have no effect on pre-installed binaries, or
those compiled following a different configuration specification.
Magic Numbers
-------------
The other common element within the header file defines a set of "magic
numbers" - one for each object within the implementation module. In fact,
this can equally well appear within the main code file, as part of the
variable structure (which will be described in the next part).
This is the technique used by mib2c, but most handcrafted modules have
tended to define these as part of the header file, probably for clarity.
The only necessity is that the names and values are distinct (or more
precisely, the values are distinct within a single variable handling routine).
In practise, they tend to be defined using integers incrementing from 1,
or as the same as the final sub-identifier of the corresponding MIB object
(or indeed both, as these are frequently themselves successive integers).
This is not mandatory, and a counter-example can be seen in the
example module, where two of the object form a sub-tree, and the corresponding
magic numbers are based on the final *two* sub-identifiers (to ensure that
the values are unique). But this construction is definitely unusual, and
the majority of modules simply use successive integers.
Header file protection
----------------------
Normally, the only other contents of the header file will be the
#ifndef/#define/#endif statements surrounding the whole file. This is used
to ensure that the header file is only included once by any source code file
(or more accurately, that there is no effect if it is inadvertantly included
a second time).
Again, as with the rest of the header file, this is generated automatically
by mib2c.
Having finished all the preparatory work (or let mib2c deal with it), the
next part starts to look at the code file that actually implements the
module.
4. Core structure of the implementation code
============================================
The core work of implementing the module is done in the C code file. As
indicated earlier, much of the detail of this will be dependent on the
particular module being implemented, and this can only be described by the
individual programmer concerned.
However, there is a fairly clearly defined framework that the implementation
will need to follow, though this varies slightly depending on the style of
the module being implemented (in particular whether it forms a table or a
series of individual values). The differences will be covered in the
following pages, but we first need to consider the overall shape of the
framework, and the elements that are common to all styles. These are
essentially the compulsory routines, the common header definitions, and
assorted initialisation code.
As with the header file, most of this will be generated automatically by
mib2c.
Standard includes
-----------------
Certain header files are either compulsory, or required so frequently that
they should be included as a matter of course. These are as follows:
#include <config.h> // local SNMP configuration details
#include "mib_module_config.h" // list of which modules are supported
#if HAVE_STDLIB_H
#include <stdlib.h>
#endif
#if HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif
#include <sys/types.h>
All of these will usually be the first files to be included.
#include "mibincl.h" // Standard set of SNMP includes
#include "util_funcs.h" // utility function declarations
#include "read_config.h" // if the module uses run-time
// configuration controls
#include "auto_nlist.h" // structures for a BSD-based
// kernel using nlist
#include "system.h"
#include "name.h" // the module-specific header
These conventionally come at the end of the list of includes. In between
will come all the standard system-provided header files required for the
library functions used in the file.
Module definition
-----------------
Much of the code defining the contents of the MIB has traditionally been
held in the header file. However, much of this has slowly migrated to the
code file, and this is now the recommended location for it (as typified by
the output of mib2c).
The main element of this is a variable structure specifying the details of
the objects implemented. This takes the form of an unconstrained array of
type struct variableN (where N is the length of the longest suffix in the
table). Thus
struct variable2 example_variables[] = {
<individual entries go here>
};
Each entry corresponds to one object in the MIB tree (or one column in the
case of table entries), and these should be listed in increasing OID order.
A single entry consists of six fields:
* a magic number (the #defined integer constant described above)
* a type indicator (from the values listed in <snmplib/snmp_impl.h>)
* an access indicator (essentially NETSNMP_OLDAPI_RWRITE or
NETSNMP_OLDAPI_RONLY)
* the name of the routine used to handle this entry
* the length of the OID suffix used, and
* an array of integers specifying this suffix (more on this in a moment)
Thus a typical variable entry would look like:
{ EXAMPLESTRING, ASN_OCTET_STR, NETSNMP_OLDAPI_RONLY,
var_example, 1, {1}}
If the magic numbers have not been defined in the header file, then they
should be defined here, usually comming immediately before the corresponding
variable entry. This is the technique used by mib2c.
Note that in practise, only certain sizes of the structure variableN
are defined (listed in <agent/var_struct.h>), being sufficient to meet the
common requirements. If your particular module needs a non-supported value,
the easiest thing is simply to use the next largest value that is supported.
The module also needs to declare the location within the MIB tree where
it should be registered. This is done using a declaration of the form
oid example_variables_oid[] = { 1,3,6,1,4,1,2021,254 }
where the contents of the array give the object identifier of the root of
the module.
Module initialisation
---------------------
Many modules require some form of initialisation before they can start
providing the necessary information. This is done by providing a routine
called init_{name} (where {name} is the name of the module).
This routine is theoretically optional, but in practise is required to
register this module with the main agent at the very least. This specifies
the list of variables being implemented (from the variableN structure)
and declare where these fit into the overall MIB tree.
This is done by using the REGISTER_MIB macro, as follows:
REGISTER_MIB( "example", example_variables, variable2,
example_variables_oid );
where "example" is used for identification purposed (and is usually the name
being used for the module), example_variables is the structure defining the
variables being implemented, variable2 is the type used for this structure,
and example_variables_oid is the location of the root.
In fact, this macro is simply a wrapper round the routine register_mib(),
but the details of this can safely be ignored, unless more control over the
registration is required.
One common requirement, particularly on older operating systems or for the
more obscure areas of the system, is to be able to read data directly from
kernel memory. The preparation for this is typically done here by one or
more statements of the form
#ifdef {NAME}_SYMBOL
auto_nlist( {NAME}_SYMBOL, 0, 0);
#endif
where {NAME}_SYMBOL is defined as part of the system-specific configuration,
to be the name of the appropriate kernel variable or data structure. (The
two 0 values are because the kernel information is simply being primed at
this point - this call will be reused later when the actual values are
required). Note that this is probably the first thing described so far which
isn't provided by mib2c!
Other possibilities for initialisation may include registering config file
directive handlers (which are documented in the read_config(5) man page), and
registering the MIB module (either in whole or in part) in the sysOR table.
The first of these is covered in the example module, and the second in many
of the other modules within the main UCD distribution.
Variable handling
-----------------
The other obligatory routine is that which actually handles a request for a
particular variable instance. This is the routine that appeared in the
variableN structure, so while the name is not fixed, it should be the same
as was used there.
This routine has six parameters, which will be described in turn.
Four of these parameters are used for passing in information about the
request, these being:
struct variable *vp;
// The entry in the variableN array from the
// header file, for the object under consideration.
// Note that the name field of this structure has been
// completed into a fully qualified OID, by prepending
// the prefix common to the whole array.
oid *name; // The OID from the request
int *length; // The length of this OID
int exact; // A flag to indicate whether this is an exact
// request (GET/SET) or an 'inexact' one (GETNEXT)
Four of the parameters are used to return information about the answer.
The function also returns a pointer to the actual data for the variable
requested (or NULL if this data is not available for any reason).
The other result parameters are:
oid *name; // The OID being returned
int *length; // The length of this OID
int *var_len; // The length of the answer being returned
WriteMethod **write_method;
// A pointer to the SET function for this variable
Note that two of the parameters (name and length) serve a dual purpose,
being used for both input and output.
The first thing that this routine needs to do is to validate the request, to
ensure that it does indeed lie in the range implemented by this particular
module. This is done in slightly different ways, depending on the style of
the module, so this will be discussed in more detail later.
At the same time, it is common to retrieve some of the information needed
for answering the query.
Then the routine uses the Magic Number field from the vp parameter to determine
which of the possible variables being implemented is being requested. This is
done using a switch statement, which should have as many cases as there are
entries in the variableN array (or more precisely, as many as specify this
routine as their handler), plus an additional default case to handle an
erroneous call.
Each branch of the switch statement needs to ensure that the return
parameters are filled in correctly, set up a (static) return variable with
the correct data, and then return a pointer to this value. These can be done
separately for each branch, or once at the start, being overridden in
particular branches if necessary.
In fact, the default validation routines make the assumption that the
variable is both read-only, and of integer type (which includes the COUNTER
and GAUGE types among others), and set the return paramaters write_method and
var_len appropriately. These settings can then be corrected for those cases
when either or both of these assumptions are wrong. Examples of this can be
seen in the example module.
EXAMPLEINTEGER is writeable, so this branch sets the write_method parameter,
and EXAMPLEOBJECTID is not an integer, so this branch sets the var_len
parameter. In the case of EXAMPLESTRING, both assumptions are wrong, so this
branch needs to set both these parameters explicitly.
Note that because the routine returns a pointer to a static result, a
suitable variable must be declared somewhere for this. Two global variables
are provided for this purpose - long_return (for integer results) and
return_buf (for other types). This latter is a generic array (of type
u_char) that can contain up to 256 bytes of data. Alternatively, static
variables can be declared, either within the code file, or local to this
particular variable routine. This last is the approach adopted by mib2c,
which defines four such local variables, (long_ret, string, objid and c64).
Mib2c requirements
------------------
Most of the code described here is generated by mib2c. The main exceptions
(which therefore need to be provided by the programmer) are
* Any initialisation, other than the basic registration
(including kernel data initialisation, config file handling, or sysOR
registration).
* Retrieving the necessary data, and setting the appropriate return
value correctly.
* The var_len (and possibly write_method) return parameters for variable
types that are not recognised by mib2c
* The contents of any write routines (see later).
Everything else should be useable as generated.
This concludes the preliminary walk-through of the general structure of the
C implementation. To fill in the details, we will need to consider the
various styles of module separately. The next part will look at scalar (i.e.
non-table based) modules.
5. 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:
* checking that the request is valid,
* setting up the OID for the result,
* and (optionally) setting up default values for the other return
parameters.
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_OID_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 by mib2c).
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.
6. Simple tables
================
Having considered the simplest style of module implementation, we now turn
our attention to the next style - a simple table. The tabular nature of
these is immediately apparent from the MIB definition file, but the
qualifier "simple" deserves a word of explanation.
A simple table, in this context, has four characteristics:
1. It is indexed by a single integer value;
2. Such indices run from 1 to a determinable maximum;
3. All indices within this range are valid;
4. The data for a particular index can be retrieved directly
(e.g. by indexing into an underlying data structure).
If any of the conditions are not met, then the table is not a pure simple
one, and the techniques described here are not applicable. The next section
of this guide will cover the more general case. (In fact, it may be possible
to use the bulk of the techniques covered here, though special handling will
be needed to cope with the invalid assumption or assumptions). Note that
mib2c assumes that all tables are simple.
As with the scalar case, the variable routine needs to provide two basic
functions - request validation and data retrieval.
Validation
----------
This is provided by the shared utility routine header_simple_table. As with
the scalar header routine, this takes the same parameters as the main
variable routine, with one addition - the maximum valid index. Mib2c
generates a dummy token for this, which must be replaced by the appropriate
value.
As with the header routine, it also returns an indication of whether the
request was valid, as well as setting up the return parameters with the
matching OID information, and defaults for var_len and write_method.
Note that in releases prior to 3.6, this job was performed by the routine
checkmib. However, the return values of this were the reverse of those for
generic_header and header_simple_table. A version of checkmib is still
available for compatability purposes, but you are encouraged to use
header_simple_table instead.
The basic code fragment (see ucd-snmp/disk.c) is therefore of the form:
unsigned char *
var_extensible_disk(vp, name, length, exact, var_len, write_method)
{
if (header_simple_table(vp,name,length,exact,var_len,write_method,numdisks)
== MATCH_FAILED)
return(NULL);
[ etc, etc, etc ]
}
Note that the maximum index value parameter does not have to be a
permanently fixed constant. It specifies the maximum valid index at the time
the request is processed, and a subsequent request may have a different
maximum.
An example of this can be seen in mibII/sysORTable.c where the table is held
purely internally to the agent code, including its size (and hence the
maximum valid index). This maximum could also be retrieved via a system
call, or via a kernel data variable.
Data Retrieval
--------------
As with the scalar case, the other required function is to retrieve the data
requested. However, given the definition of a simple table this is simply a
matter of using the single, integer index sub-identifier to index into an
existing data structure. This index will always be the last index of the OID
returned by header_simple_table, so can be obtained as name[*length-1].
A good example of this type of table can be seen in ucd-snmp/disk.c
With some modules, this underlying table may be relatively large, or only
accessible via a slow or cumbersome interface. The implementation described
so far may prove unacceptably slow, particularly when walking a MIB tree
requires the table to be loaded afresh for each variable requested.
In these circumstances, a useful technique is to cache the table when it is
first read in, and use that cache for subsequent requests. This can be done
by having a separate routine to read in the table. This uses two static
variables, one a structure or array for the data itself, and the other an
additional timestamp to indicate when the table was last loaded. When a call
is made to this routine to "read" the table, it can first check whether the
cached table is "new enough". If so, it can return immediately, and the
system will use the cached data.
Only if the cached version is sufficiently old that it's probably out of
date, is it necessary to retrieve the current data, updating the cached
version and the timestamp value.
This is particularly useful if the data itself is relatively static, such as
a list of mounted filesystems. There is an example of this technique in the
Host Resources implementation.
As with the scalar case, mib2c simply provides placeholder dummy return
values. It's up to the programmer to fill in the details.
The next part concludes the examination of the detailed implementation by
looking at more general tables.
7. General Tables
=================
Some table structures are not suitable for the simple table approach, due to
the failure of one or more of the assumptions listed earlier. Perhaps they
are indexed by something other than a single integer (such as a 4-octet IP
address), or the maximum index is not easily determinable (such as the
interfaces table), or not all indices are valid (running software), or the
necessary data is not directly accessible (interfaces again).
In such circumstances, a more general approach is needed. In contrast with
the two styles already covered, this style of module will commonly combine
the two functions of request validation and data retrieval. Note that mib2c
will assume the simple table case, and this will need to be corrected.
General table algorithm
-----------------------
The basic algorithm is as follows:
Perform any necessary initialization, then walk through the
underlying instances, retrieving the data for each one, until the
desired instance is found. If no valid entry is found, return
failure.
For an exact match (GET and similar), identifying the desired instance is
trivial - construct the OID (from the 'vp' variable parameter and the index
value or values), and see whether it matches the requested OID.
For GETNEXT, the situation is not quite so simple. Depending on the
underlying representation of the data, the entries may be returned in the
same order as they should appear in the table (i.e. lexically increasing by
index). However, this is not guaranteed, and the natural way of retrieving
the data may be in some "random" order. In this case, then the whole table
needs to be traversed for each request. in order to determine the
appropriate successor.
This random order is the worst case, and dictates the structure of the code
used in most currently implemented tables. The ordered case can be regarded
as a simplification of this more general one.
The algorithm outlined above can now be expanded into the following
pseudo-code:
Init_{Name}_Entry(); // Perform any necessary initialisation
while (( index = Get_Next_{Name}_Entry() ) != EndMarker ) {
// This steps through the underlying table,
// returning the current index,
// or some suitable end-marker when all
// the entries have been examined.
// Note that this routine should also return the
// data for this entry, either via a parameter
// or using some external location.
construct OID from vp->name and index
compare new OID and request
if valid {
save current data
if finished // exact match, or ordered table
break; // so don't look at any more entries
}
// Otherwise, we need to loop round, and examine
// the next entry in the table. Either because
// the entry wasn't valid for this request,
// or the entry was a possible "next" candidate,
// but we don't know that there isn't there's a
// better one later in the table.
}
if no saved data // Nothing matched
return failure
// Otherwise, go on to the switch handling
// we've already covered in the earlier styles.
This is now very close to the actual code used in many current
implementations (such as the the routine header_ifEntry in
mibII/interfaces.c). Notice that the pseudo-code fragment if valid expands
in practise to
if ((exact && (result == 0)) ||
// GET request, and identical OIDs
(!exact && (result < 0)) )
// GETNEXT, and candidate OID is later
// than requested OID.
This is a very common expression, that can be seen in most of the table
implementations.
Notice also that the interfaces table returns immediately the first valid
entry is found, even for GETNEXT requests. This is because entries are
returned in lexical order, so the first succeeding entry will be the one
that's required.
(As an aside, this also means that the underlying data can be saved
implicitly within the 'next entry' routine - not very clean, but it saves
some unnecessary copying).
The more general case can be seen in the TCP and UDP tables (see mibII/tcp.c
and mibII/udp.c). Here, the if valid fragment expands to:
if ( exact && (result == 0)) {
// save results
break;
}
else if (!exact && (result < 0)) {
if ( .... ) { // no saved OID, or this OID
// precedes the saved OID
// save this OID into 'lowest'
// save the results into Lowinpcb
// don't break, since we still need to look
// at the rest of the table
}
}
The GET match handling is just as we've already seen - is this the requested
OID or not. If so, save the results and move on to the switch statement.
The GETNEXT case is more complicated. As well as considering whether this
is a possible match (using the same test we've already seen), we also have to
check whether this is a better match than anything we've already seen. This
is done by comparing the current candidate (newname) with the best match found
so far (lowest).
Only if this extra comparison shows that the new OID is earlier than the
saved one, do we need to save both the new OID, and any associated data
(such as the inpcb block, and state flag). But having found one better
match, we don't know that there isn't an even better one later on. So we
can't break out of the enclosing loop - we need to keep going and examine
all the remaining entries of the table.
These two cases (the TCP and UDP tables) also show a more general style of
indexing. Rather than simply appending a single index value to the OID
prefix, these routines have to add the local four-octet IP address plus port
(and the same for the remote end in the case of the TCP table). This is the
purpose of the op and cp section of code that precedes the comparison.
These two are probably among the most complex cases you are likely to
encounter. If you can follow the code here, then you've probably cracked the
problem of understanding how the agent works.
Finally, the next part discusses how to implement a writable (or SETable)
object in a MIB module.
8. How to implement a SETable object
====================================
Finally, the only remaining area to cover is that of setting data - the
handling of SNMPSET. Particular care should be taken here for two reasons.
Firstly, any errors in the earlier sections can have limited effect. The
worst that is likely to happen is that the agent will either return invalid