Code

the number of COMPUTE rpn nodes is architecture dependent. calculate the right number...
[rrdtool-all.git] / contrib / snmpstats / SNMPstats.pl
1 #!/usr/bin/perl -w
3 # Q: Who wrote this?
4 # Bill Nash - billn@billn.net / billn@gblx.net
5 #
6 # Q: Why?
7 # SNMP retrieval and storage of interface utilization, ala MRTG.
8 #
9 # Q: Is this a supported utility?
10 # Barely. That means if there's a serious problem with it, you can email me. I'll take feature requests
11 # provided they're presented in an intelligent manner. I will NOT write scripts for you. There's a plethora
12 # of information available to you, stop being lazy and do it yourself. Mostly, I wrote this for myself. I
13 # released it to the community at large because it's useful. Your mileage may vary. This code carries no 
14 # warranty and I'm not responsible if you do something stupid with it.
15 #
16 # Q: Why does the author sound like a grumpy curmudgeon?
17 # Because I'm releasing a utility to the public, and I detest people. I read the MRTG lists. I know what you 
18 # people are and are not capable of. I could jump up on my soapbox and rant about the general laziness of people,
19 # but no one will care. The user base at large is full of lazy bastards who just want someone else to create something
20 # that does exactly what they want, with as little effort required on their part.
21 #
22 # Q: Is it safe to ask questions about this utility?
23 # I will be more than happy to entertain discussions about this utility, provided:
24 #       It's a discussion of perl mechanics, and the person asking the question knows something about Perl.
25 #       It's a discussion of SNMP mechanics, and the person asking the question isn't asking where to find Mibs/objects.
26 #       You're a Playboy Bunny and you'd like to meet me for dinner.
27 #
28 # Q: Your code sucks, billn, why does this do [such and such], and why didn't you do condense [this and this]?
29 # This is intended to be a simple utility. No fancy obsfucation, no serious attention to efficiency. The only real creative 
30 # parts are using ifDescr/ifName as an interface basis (which offsets the nasty ifIndex shift problem by using ifIndex has a 
31 # value of the key, ifDescr/ifName, instead of vice versa. The ifIndex can change all it wants. Don't go saying 'Well, what if 
32 # interface name changes?', because I'll just say "Then it's a new interface. Cope."
33 # Also, by NOT obfuscating functions and keeping things simple, I'd hope people looking at this script that aren't fully versed
34 # in the intricacies and foibles of SNMP, PERL and RRD will have an easier time grasping the concepts, and maybe learn a bit from 
35 # this. Much of the code contained in here is interchangable, data sources can be substituted left and right, and I fully expect
36 # someone to hack this into a shining pearl of relative usefulness on a regular basis. It's not the end all, be all of SNMP pollers,
37 # but I expect it'll find widespread use.
39 $local_dir = "/usr/local/rrdtool-1.0.28";       # Where this script lives
40 $rrd_store = "$local_dir/rrd";          # Where to keep our rrd datastores
42 $debug = 0;
44 # This is Net::SNMP-2.00. It's not included with this script. Try CPAN.
45 use Net::SNMP;
47 # RRD Perl module. If you don't have it, why are you here?
48 use RRDs;
50 # This piece can be ripped out and subbed for any number of data storage methods. This is a simple method
51 # that works for those handling only a few devices. IP addresses are important because I don't use hostname
52 # matches for the SNMP calls. This eliminates DNS dependancies, but does require you to maintain your code or
53 # host registries.
55 $devices{"Hades"}{'ip_address'} = "10.0.0.254"; # my switch
56 $devices{"Hades"}{'snmp_read'} = "public";
57 $devices{"Bifrost"}{'ip_address'} = "10.0.0.253"; # my router
58 $devices{"Bifrost"}{'snmp_read'} = "public";
60 # Standard SNMP mib2 jazz. Feel free to edit. YMMV.
62 # Variables from the %oids hash we'll be referencing later. It's easier to call them by a name.
63 # What, you think I'm gonna memorize SNMP oids? =P
65 @poll_int = (
66                 "ifDescr",
67                 "ifOperStatus",
68                 "ifAlias",
69                 "ifInErrors",
70                 "ifInOctets",
71                 "ifOutErrors",
72                 "ifOutOctets",
73                 "ifSpeed"
74 );
76 %oids = (
77         sysDescr              => "1.3.6.1.2.1.1.1.0",
78         sysName               => "1.3.6.1.2.1.1.5.0",
79         sysUptime             => "1.3.6.1.2.1.1.3.0",
80         ifNumber              => "1.3.6.1.2.1.2.1.0",
81         #ifDescr               => "1.3.6.1.2.1.2.2.1.2",
82         ifType                => "1.3.6.1.2.1.2.2.1.3",
83         ifSpeed               => "1.3.6.1.2.1.2.2.1.5",
84         ifPhysAddress         => "1.3.6.1.2.1.2.2.1.6",
85         ifAdminStatus         => "1.3.6.1.2.1.2.2.1.7", 
86         ifOperStatus          => "1.3.6.1.2.1.2.2.1.8",
87         ifAlias               => "1.3.6.1.2.1.31.1.1.1.18",
88         ifInErrors            => "1.3.6.1.2.1.2.2.1.14",
89         ifInOctets            => "1.3.6.1.2.1.2.2.1.10",
90         ifInUnkProtos         => "1.3.6.1.2.1.2.2.1.15",
91         ifLastChange          => "1.3.6.1.2.1.2.2.1.19",
92         ifDescr                => "1.3.6.1.2.1.31.1.1.1.1", # was ifXName, subbed for ifDescr
93         ifOutDiscards         => "1.3.6.1.2.1.2.2.1.19",
94         ifOutErrors           => "1.3.6.1.2.1.2.2.1.20",
95         ifOutOctets           => "1.3.6.1.2.1.2.2.1.16"
96 );
98 while(1) {
99         $start = time;
101         foreach $device_name (keys %devices) {
102                 undef(%ifAdmin);
103                 # Establish an snmp session with the device
104                 my($session, $error) = Net::SNMP->session(
105                                             Hostname  => $devices{$device_name}{'ip_address'},
106                                             Community => $devices{$device_name}{'snmp_read'},
107                                             Translate => 1,
108                                             VerifyIP  => 1
109                 );
111         # This example may seem a bit long and drawn out, but it's better for a clear view of how the procedure works
112         # It's entirely possible (and more efficient) to restructure this into a tight bundle of reusable code.
113                 if ($session) {
114                         print "$device_name: SNMP Session established ($device_name, $devices{$device_name}{'ip_address'})\n" if ($debug);
116                 # First step, find all the administratively active interfaces. Typically, this should be the ONLY
117                 # table that takes a walk across all interfaces. If you're doing smart and clean device management,
118                 # all unused/undesignated interfaces should be admin'd down and scrubbed of configs. If you don't
119                 # maintain this kind of device policy, don't cry to me because things take longer than you expect.
121                 # For the sake of efficiency, I should note here that this set of data doesn't HAVE to be generated with an SNMP poll
122                 # You can have an entirely external management system here that dictates what interfaces are tracked. You can rip this
123                 # chunk out and replace it with something else entirely.
125                 #print "Retrieving ifAdminStatus table: $oids{'ifAdminStatus'}\n" if ($debug);
126                         $response = $session->get_table($oids{'ifAdminStatus'});
127                         if($error_message = $session->error) {
128                                 if($error_message eq "Requested table is empty" ||
129                                         $error_message eq "Recieved SNMP noSuchName(2) error-status at error-index 1") {}
130                                 else {
131                                         print STDERR "ifAdmin table get failed ($device_name: $oids{'ifAdminStatus'}): $error_message\n"
132                                 }       # end if
133                                 next; # Can't get an ifAdminStatus table? No active interfaces or a borked SNMP engine. Next!
134                         } # end if
136                         %array = %{$response};
137                         foreach $key (keys %array) {
139                                 $ifIndex = $key;
140                                 $ifIndex =~ s/^$oids{'ifAdminStatus'}.//g;
142                         # Hash the ifAdminStatus data if the status is 1. We aren't going to bother with any 
143                         # interfaces that aren't set active.
144                         # For the curious, possible values here are:
145                         # @OperStatus=("null", "Up", "Down", "Looped", "null", "Dormant", "Missing");
147                                 $ifAdmin{$ifIndex} = $array{$key};
148                                 #print "$device_name: ifIndex $ifIndex, ifAdmin $array{$key} $ifAdmin{$ifIndex}\n" if ($debug);
149                         } #end foreach
151                         # Cycle through all The admin'd active interfaces, by ifIndex
152                         foreach $ifIndex (keys %ifAdmin) {
153                                 undef(@interface_rrd);
154                                 next if ($ifAdmin{$ifIndex} != 1);
155                         # Cycle through all the objects we want to track for each interface. This 
156                         # is a highly reusable set of code, set up to perform the same task repeatedly for 
157                         # (potentially long) lists of variables.
158                                 foreach $object (@poll_int) {
159                                 # get the numeric oid values from the oids table
160                                         $object_id = $oids{$object};
162                                 # go get the object.
163                                         $response = $session->get_request("$object_id.$ifIndex");
164                                         if($error_message = $session->error) {
165                                                 if($error_message eq "Recieved SNMP noSuchName(2) error-status at error-index 1") {
166                                                 # It's a common occurence to poll an interface for an object that it
167                                                 # doesn't support, so we'll just U the object.
168                                                         $data{$device_name}{$ifIndex}{$object} = "U";
169                                                 } #end if
171                                         # Whatever the object was, it didn't want to be 'gotten', so screw it.
172                                                 print STDERR "Object get failed ($device_name: $object_id.$ifIndex):$error_message\n" if ($debug);
173                                                 next;
174                                         } #end if
176                                         %array = %{$response};
178                                 # Shucks, got data, get to work. This chunk of code is pretty generic, and you'll 
179                                 # recognize it from up above. I *could* use a single iteration here, but better save
180                                 # in case the snmp engine did something hokey, or we used a table base variable in the get.
181                                 # The multilayer hash prolly makes some of you twitch to see, but hey, if you don't like it,
182                                 # why are you reading my code to begin with? It works, take a hike.
183                                 # Anyway, it's an extensible memory structure that doesn't care what you're stuffing into it.
185                                         foreach $key (keys %array) {
186                                                 $ifIndex = $key;
187                                                 $ifIndex =~ s/^$oids{$object}.//g;
189                                                 $data{$device_name}{$ifIndex}{$object} = $array{$key};
190                                                 #print "$device_name: ifIndex $ifIndex, $object = $data{$device_name}{$ifIndex}{$object}\n";
191                                         } #end foreach
192                                 } #end foreach
193                         } #end foreach
195                 # Alright, so at this point, we should have a full set of data (whatever we requested) for 
196                 # each active interface.
197                 # This whole next section is all about what we do with any given piece of data, so if you're doing
198                 # customization beyond what I've included, here's your sandbox, here's your shovel. Go build me a Buick.
200                 # My primary goal for this utility is low overhead interface utilization tracking for my router and switch.
201                 # In combination with RRDtool's graphing abilities, poof, it's a skimpy but solid (and extensible) replacement
202                 # for MRTG. Don't get me wrong, I like MRTG, but RRDtool a lot easier to do flexible things with. The fact
203                 # that this whole piece is in Perl provides a working template for bigger and crazier things, like using 
204                 # a real SQL db for tracking port data, or real time data feeds to Linus knows what. With these things in
205                 # mind, let's start tossing some data.
207                 #        ifSpeed               => "1.3.6.1.2.1.2.2.1.5",
208                 # Since we're doing traffic graphing, it's helpful to know the size of the pipe we're tracking.
210                 #       ifOperStatus          => "1.3.6.1.2.1.2.2.1.8",
211                 # If the interface is down for some reason, it'd be good to have a way to represent that.
213                 #       ifAlias               => "1.3.6.1.2.1.31.1.1.1.18",
214                 # ifAlias is usually a human supplied interface description. 
216                 #       ifInErrors            => "1.3.6.1.2.1.2.2.1.14",
217                 #       ifInOctets            => "1.3.6.1.2.1.2.2.1.10",
218                 #       ifInUnkProtos         => "1.3.6.1.2.1.2.2.1.15",
219                 #       ifOutDiscards         => "1.3.6.1.2.1.2.2.1.19",
220                 #       ifOutErrors           => "1.3.6.1.2.1.2.2.1.20",
221                 #       ifOutOctets           => "1.3.6.1.2.1.2.2.1.16"
222                 # These should be pretty obvious. No, that's not short for Uncle Protos.
224                 #       ifDescr               => "1.3.6.1.2.1.2.2.1.2",
225                 # This is usually the name for an interface. Very important variable.
226                 # Since I'm testing with a cisco catalyst, I've switched ifDescr for ifName/ifXName, up top. Less pain.
228                 # We need a place to store this stuff, so let's check out storage structures
230                         foreach $device_name (keys %data) {
231                                 #print "Generating/feeding data for $device_name\n";
232                                 foreach $ifIndex (keys %{$data{$device_name}}) {
234                                         $ifDescr = $data{$device_name}{$ifIndex}{'ifDescr'};
235                                         if ($ifDescr eq "") {
236                                                 #print "$device_name ifIndex $ifIndex apparantly has a null ifDescr -> [$ifDescr], skipping\n";
237                                                 next;
238                                         } # end if
240                         # If you recognize where I stole these from, you may already know me as '[tHUg]Heartless'
241                         # I prefer the Aug and the TMP, and I fear no AWP. =)
242                         # This set of regexp's is for scrubbing potentially exciting characters from interface names before 
243                         # using them as the basis for storing files. Some OS's and file systems may object to some of these 
244                         # characters, so, better safe than annoyed.
245                         # You'll note I don't provide facilities for reverting this. I just collect the stuff. Display is your problem.
247                                         $ifDescr =~ s/ /_/g;
248                                         $ifDescr =~ s/\=/\[EQUAL\]/g;
249                                         $ifDescr =~ s/\,/\[CMA\]/g;
250                                         $ifDescr =~ s/;/\[SMICLN\]/g;
251                                         $ifDescr =~ s/:/\[CLN\]/g;
252                                         $ifDescr =~ s/\"/\[DBLQT\]/g;
253                                         $ifDescr =~ s/\'/\[SNGLQT\]/g;
254                                         $ifDescr =~ s/\{/\[LB2\]/g;
255                                         $ifDescr =~ s/\}/\[RB2\]/g;
256                                         $ifDescr =~ s/\+/\[PLS\]/g;
257                                         $ifDescr =~ s/\-/\[DSH\]/g;
258                                         $ifDescr =~ s/\(/\[LPRN\]/g;
259                                         $ifDescr =~ s/\)/\[RPRN\]/g;
260                                         $ifDescr =~ s/\*/\[STR\]/g;
261                                         $ifDescr =~ s/\&/\[AND\]/g;
262                                         $ifDescr =~ s/\|/\[PIPE\]/g;
263                                         $ifDescr =~ s/\\/\[BSLSH\]/g;
264                                         $ifDescr =~ s/\//\[FSLSH\]/g;
265                                         $ifDescr =~ s/\?/\[QUESTN\]/g;
266                                         $ifDescr =~ s/\</\[LT\]/g;
267                                         $ifDescr =~ s/\>/\[GT\]/g;
268                                         $ifDescr =~ s/\./\[DOT\]/g;
269                                         $ifDescr =~ s/\!/\[XCLM\]/g;
270                                         $ifDescr =~ s/\@/\[AT\]/g;
271                                         $ifDescr =~ s/\#/\[PND\]/g;
272                                         $ifDescr =~ s/\$/\[DLLR\]/g;
273                                         $ifDescr =~ s/\%/\[\PRCNT\]/g;
274                                         $ifDescr =~ s/\^/\[CRT\]/g;
275                 
276                                         if ( -e "$rrd_store/$device_name-$ifDescr.rrd") {
277                                         # Uh, hey, it's there. Don't worry, be happy.
278                                         }
279                                         else {  # Oh, damn, it isn't, better create it.
280                                 
281                         # Knowing the speed of the interface, generally reported by SNMP in bits per second,
282                         # we can fairly accurately determine how long it could take that counter to roll over,
283                         # if it's a 32 bit counter.
284                         # So, we'll use that info in creating the interface data. You may recognize these variables
285                         # from the RRD tutorial docs, which were further derived from MRTG. I reuse them both because
286                         # I'm lazy and so people will recognize what to hack on if they've beat up MRTG before.
288                                                 if ($speed = $data{$device_name}{$ifIndex}{'ifSpeed'}) {
289                                                         print "$device_name: Found $speed speed for $ifIndex\n";
290                                                 }
291                                                 else {
292                                                         $speed = "U";
293                                                 }
294         
295                                                 @interface_rrd = (
296                                                 "DS:InBits:COUNTER:600:0:$speed",
297                                                 "DS:OutBits:COUNTER:600:0:$speed",
298                                                 "RRA:AVERAGE:0.5:1:600",
299                                                 "RRA:AVERAGE:0.5:6:700",
300                                                 "RRA:AVERAGE:0.5:24:775",
301                                                 "RRA:AVERAGE:0.5:288:797",
302                                                 "RRA:MAX:0.5:1:600",
303                                                 "RRA:MAX:0.5:6:700",
304                                                 "RRA:MAX:0.5:24:775",
305                                                 "RRA:MAX:0.5:288:797"
306                                                 );
307         
308                         # I feed the array to the create argument here, so it's easier to alter the rrd
309                         # creation by just changing entries in the array above. Generic and reusable.
311                                                 if(RRDs::create ("$rrd_store/$device_name-$ifDescr.rrd",
312                                                          "--step=300", 
313                                                          @interface_rrd)) {
314                                                         print "Built RRd for $ifDescr\n";
315                                                 }
316                                                 else {
317                                                         $ERR=RRDs::error;
318                                                         print "RRd build for $ifDescr appears to have failed: $ERR\n";
319                                                         next;
320                                                 }
321                                         }
324                         # Do some calculations.
326                                         $data{$device_name}{$ifIndex}{InBits} = $data{$device_name}{$ifIndex}{ifInOctets} * 8;
327                                         $data{$device_name}{$ifIndex}{OutBits} = $data{$device_name}{$ifIndex}{ifOutOctets} * 8;
329                         # Feed the RRD our data.
331                                         $rrdfeed = join ":", ("N", 
332                                                 $data{$device_name}{$ifIndex}{InBits},
333 #                                               $data{$device_name}{$ifIndex}{IfInErrors},
334                                                 $data{$device_name}{$ifIndex}{OutBits},
335 #                                               $data{$device_name}{$ifIndex}{IfOutErrors},
336                                         );
338                                         RRDs::update ("$rrd_store/$device_name-$ifDescr.rrd",
339                                                 "--template", "InBits:OutBits", 
340                                                 "$rrdfeed"); 
342                                         if($ERR=RRDs::error) {
343                                                 print STDERR "$rrd_store/$device_name-$ifDescr.rrd update failed: $ERR\n";
344                                         }
345                                         else {
346                                                 #print "$rrd_store/$device_name-$ifDescr.rrd updated\n" if ($debug);
347                                         }
348                                 }
349                         }       # yeah, it's sloppy, sue me.
350                 }
351                 else {
352                 # Abort abort abort, no go no go. uNF. =)
353                         print STDERR "$device_name: SNMP Session failed: $error\n";
354                 }
355         }
357         $end = time;
359         $duration = $end - $start;
360         $sleep_period = 300 - $duration;
361         if($sleep_period > 0) { sleep($sleep_period) }
362         undef(%data);