import torrus 1.0.9
[freeside.git] / torrus / perllib / Torrus / Collector / SNMP.pm
1 #  Copyright (C) 2002-2007  Stanislav Sinyagin
2 #
3 #  This program is free software; you can redistribute it and/or modify
4 #  it under the terms of the GNU General Public License as published by
5 #  the Free Software Foundation; either version 2 of the License, or
6 #  (at your option) any later version.
7 #
8 #  This program is distributed in the hope that it will be useful,
9 #  but WITHOUT ANY WARRANTY; without even the implied warranty of
10 #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 #  GNU General Public License for more details.
12 #
13 #  You should have received a copy of the GNU General Public License
14 #  along with this program; if not, write to the Free Software
15 #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
16
17 # $Id: SNMP.pm,v 1.1 2010-12-27 00:03:58 ivan Exp $
18 # Stanislav Sinyagin <ssinyagin@yahoo.com>
19
20 package Torrus::Collector::SNMP;
21
22 use Torrus::Collector::SNMP_Params;
23 use Torrus::ConfigTree;
24 use Torrus::Log;
25 use Torrus::SNMP_Failures;
26
27 use strict;
28 use Net::hostent;
29 use Socket;
30 use Net::SNMP qw(:snmp);
31 use Math::BigInt;
32
33
34 # Register the collector type
35 $Torrus::Collector::collectorTypes{'snmp'} = 1;
36
37
38 # List of needed parameters and default values
39
40 $Torrus::Collector::params{'snmp'} = {
41     'snmp-ipversion'    => undef,
42     'snmp-transport'    => undef,
43     'snmp-version'      => undef,
44     'snmp-port'         => undef,
45     'snmp-community'    => undef,
46     'snmp-username'     => undef,
47     'snmp-authkey'      => undef,
48     'snmp-authpassword' => undef,
49     'snmp-authprotocol' => 'md5',
50     'snmp-privkey'      => undef,
51     'snmp-privpassword' => undef,
52     'snmp-privprotocol' => 'des',
53     'snmp-timeout'      => undef,
54     'snmp-retries'      => undef,
55     'domain-name'       => undef,
56     'snmp-host'         => undef,
57     'snmp-localaddr'    => undef,
58     'snmp-localport'    => undef,
59     'snmp-object'       => undef,
60     'snmp-oids-per-pdu' => undef,
61     'snmp-object-type'  => 'OTHER',
62     'snmp-check-sysuptime' => 'yes',
63     'snmp-max-msg-size' => undef,
64     'snmp-ignore-mib-errors' => undef,
65     };
66
67 my $sysUpTime = '1.3.6.1.2.1.1.3.0';
68
69 # Hosts that are running SNMPv1. We do not reresh maps on them, as
70 # they are too slow
71 my %snmpV1Hosts;
72
73 # SNMP tables lookup maps
74 my %maps;
75
76 # Old lookup maps, used temporarily during refresh cycle
77 my %oldMaps;
78
79 # How frequent we refresh the SNMP mapping
80 our $mapsRefreshPeriod;
81
82 # Random factor in refresh period
83 our $mapsRefreshRandom;
84
85 # Time period after configuration re-compile when we refresh existing mappings
86 our $mapsUpdateInterval;
87
88 # how often we check for expired maps
89 our $mapsExpireCheckPeriod;
90
91 # expiration time for each map
92 my %mapsExpire;
93
94 # Lookups scheduled for execution
95 my %mapLookupScheduled;
96
97 # SNMP session objects for map lookups
98 my @mappingSessions;
99
100
101 # Timestamps of hosts last found unreachable
102 my %hostUnreachableSeen;
103
104 # Last time we tried to reach an unreachable host
105 my %hostUnreachableRetry;
106
107 # Hosts that were deleted because of unreachability for too long
108 my %unreachableHostDeleted;
109
110
111 our $db_failures;
112
113 # Flush stats after a restart or recompile
114 $Torrus::Collector::initCollectorGlobals{'snmp'} =
115     \&Torrus::Collector::SNMP::initCollectorGlobals;
116
117 sub initCollectorGlobals
118 {
119     my $tree = shift;
120     my $instance = shift;
121     
122     if( not defined( $db_failures ) )
123     {
124         $db_failures =
125             new Torrus::SNMP_Failures( -Tree => $tree,
126                                        -Instance => $instance,
127                                        -WriteAccess => 1 );
128     }
129
130     if( defined( $db_failures ) )
131     {
132         $db_failures->init();
133     }
134
135     # re-init counters and collect garbage
136     %oldMaps = ();
137     %hostUnreachableSeen = ();
138     %hostUnreachableRetry = ();
139     %unreachableHostDeleted = ();
140     
141     # Configuration re-compile was probably caused by new object instances
142     # appearing on the monitored devices. Here we force the maps to refresh
143     # soon enough in order to catch up with the changes
144
145     my $now = time();    
146     foreach my $maphash ( keys %mapsExpire )
147     {
148         $mapsExpire{$maphash} = int( $now + rand( $mapsUpdateInterval ) );
149     }    
150 }
151
152
153 # This is first executed per target
154
155 $Torrus::Collector::initTarget{'snmp'} = \&Torrus::Collector::SNMP::initTarget;
156
157
158
159 sub initTarget
160 {
161     my $collector = shift;
162     my $token = shift;
163
164     my $tref = $collector->tokenData( $token );
165     my $cref = $collector->collectorData( 'snmp' );
166
167     $collector->registerDeleteCallback
168         ( $token, \&Torrus::Collector::SNMP::deleteTarget );
169
170     my $hostname = getHostname( $collector, $token );
171     if( not defined( $hostname ) )
172     {
173         return 0;
174     }
175
176     $tref->{'hostname'} = $hostname;
177     
178     return Torrus::Collector::SNMP::initTargetAttributes( $collector, $token );
179 }
180
181
182 sub initTargetAttributes
183 {
184     my $collector = shift;
185     my $token = shift;
186
187     &Torrus::DB::checkInterrupted();
188
189     my $tref = $collector->tokenData( $token );
190     my $cref = $collector->collectorData( 'snmp' );
191
192     my $hostname = $tref->{'hostname'};
193     my $port = $collector->param($token, 'snmp-port');
194     my $version = $collector->param($token, 'snmp-version');
195
196     my $community;
197     if( $version eq '1' or $version eq '2c' )
198     {
199         $community = $collector->param($token, 'snmp-community');
200     }
201     else
202     {
203         # We use community string to identify the agent.
204         # For SNMPv3, it's the user name
205         $community = $collector->param($token, 'snmp-username');
206     }
207
208     my $hosthash = join('|', $hostname, $port, $community);
209     $tref->{'hosthash'} = $hosthash;
210
211     if( $version eq '1' )
212     {
213         $snmpV1Hosts{$hosthash} = 1;
214     }
215     
216     # If the object is defined as a map, retrieve the whole map
217     # and cache it.
218
219     if( isHostDead( $collector, $hosthash ) )
220     {
221         return 0;
222     }
223         
224     if( not checkUnreachableRetry( $collector, $hosthash ) )
225     {
226         $cref->{'needsRemapping'}{$token} = 1;
227         return 1;
228     }
229     
230     my $oid = $collector->param($token, 'snmp-object');
231     $oid = expandOidMappings( $collector, $token, $hosthash, $oid );
232
233     if( not $oid )
234     {
235         if( $unreachableHostDeleted{$hosthash} )
236         {
237             # we tried our best, but the target is dead
238             return 0;
239         }
240         else
241         {
242             # we return OK status, to let the storage initiate
243             $cref->{'needsRemapping'}{$token} = 1;
244             return 1;
245         }
246     }
247     elsif( $oid eq 'notfound' )
248     {
249         return 0;
250     }
251
252     # Collector should be able to find the target
253     # by host, port, community, and oid.
254     # There can be several targets with the same host|port|community+oid set.
255
256     $cref->{'targets'}{$hosthash}{$oid}{$token} = 1;
257     $cref->{'activehosts'}{$hosthash} = 1;
258
259     $tref->{'oid'} = $oid;
260
261     $cref->{'oids_per_pdu'}{$hosthash} =
262         $collector->param($token, 'snmp-oids-per-pdu');
263
264     if( $collector->param($token, 'snmp-object-type') eq 'COUNTER64' )
265     {
266         $cref->{'64bit_oid'}{$oid} = 1;
267     }
268
269     if( $collector->param($token, 'snmp-check-sysuptime') eq 'no' )
270     {
271         $cref->{'nosysuptime'}{$hosthash} = 1;
272     }
273
274     if( $collector->param($token, 'snmp-ignore-mib-errors') eq 'yes' )
275     {
276         $cref->{'ignoremiberrors'}{$hosthash}{$oid} = 1;
277     }
278     
279     return 1;
280 }
281
282
283 sub getHostname
284 {
285     my $collector = shift;
286     my $token = shift;
287
288     my $cref = $collector->collectorData( 'snmp' );
289
290     my $hostname = $collector->param($token, 'snmp-host');
291     my $domain = $collector->param($token, 'domain-name');
292     
293     if( length( $domain ) > 0 and
294         index($hostname, '.') < 0 and
295         index($hostname, ':') < 0 )
296     {
297         $hostname .= '.' . $domain;
298     }
299     
300     return $hostname;
301 }
302
303
304 sub snmpSessionArgs
305 {
306     my $collector = shift;
307     my $token = shift;
308     my $hosthash = shift;
309
310     my $cref = $collector->collectorData( 'snmp' );
311     if( defined( $cref->{'snmpargs'}{$hosthash} ) )
312     {
313         return $cref->{'snmpargs'}{$hosthash};
314     }
315
316     my $transport = $collector->param($token, 'snmp-transport') . '/ipv' .
317         $collector->param($token, 'snmp-ipversion');
318     
319     my ($hostname, $port, $community) = split(/\|/o, $hosthash);
320
321     my $version = $collector->param($token, 'snmp-version');
322     my $ret = [ -domain       => $transport,
323                 -hostname     => $hostname,
324                 -port         => $port,
325                 -timeout      => $collector->param($token, 'snmp-timeout'),
326                 -retries      => $collector->param($token, 'snmp-retries'),
327                 -version      => $version ];
328     
329     foreach my $arg ( qw(-localaddr -localport) )
330     {
331         if( defined( $collector->param($token, 'snmp' . $arg) ) )
332         {
333             push( @{$ret}, $arg, $collector->param($token, 'snmp' . $arg) );
334         }
335     }
336             
337     if( $version eq '1' or $version eq '2c' )
338     {
339         push( @{$ret}, '-community', $community );
340     }
341     else
342     {
343         push( @{$ret}, -username, $community);
344
345         foreach my $arg ( qw(-authkey -authpassword -authprotocol
346                              -privkey -privpassword -privprotocol) )
347         {
348             if( defined( $collector->param($token, 'snmp' . $arg) ) )
349             {
350                 push( @{$ret},
351                       $arg, $collector->param($token, 'snmp' . $arg) );
352             }
353         }
354     }
355
356     $cref->{'snmpargs'}{$hosthash} = $ret;
357     return $ret;
358 }
359               
360
361
362 sub openBlockingSession
363 {
364     my $collector = shift;
365     my $token = shift;
366     my $hosthash = shift;
367
368     my $args = snmpSessionArgs( $collector, $token, $hosthash );
369     my ($session, $error) =
370         Net::SNMP->session( @{$args},
371                             -nonblocking  => 0,
372                             -translate    => ['-all', 0, '-octetstring', 1] );
373     if( not defined($session) )
374     {
375         Error('Cannot create SNMP session for ' . $hosthash . ': ' . $error);
376     }
377     else
378     {
379         my $maxmsgsize = $collector->param($token, 'snmp-max-msg-size');
380         if( defined( $maxmsgsize ) and $maxmsgsize > 0 )
381         {
382             $session->max_msg_size( $maxmsgsize );
383         }
384     }
385     
386     return $session;
387 }
388
389 sub openNonblockingSession
390 {
391     my $collector = shift;
392     my $token = shift;
393     my $hosthash = shift;
394
395     my $args = snmpSessionArgs( $collector, $token, $hosthash );
396     
397     my ($session, $error) =
398         Net::SNMP->session( @{$args},
399                             -nonblocking  => 0x1,
400                             -translate    => ['-timeticks' => 0] );
401     if( not defined($session) )
402     {
403         Error('Cannot create SNMP session for ' . $hosthash . ': ' . $error);
404         return undef;
405     }
406     
407     if( $collector->param($token, 'snmp-transport') eq 'udp' )
408     {
409         # We set SO_RCVBUF only once, because Net::SNMP shares
410         # one UDP socket for all sessions.
411         
412         my $sock_name = $session->transport()->sock_name();
413         my $refcount = $Net::SNMP::Transport::SOCKETS->{
414             $sock_name}->[&Net::SNMP::Transport::_SHARED_REFC()];
415                                                                       
416         if( $refcount == 1 )
417         {
418             my $buflen = int($Torrus::Collector::SNMP::RxBuffer);
419             my $socket = $session->transport()->socket();
420             my $ok = $socket->sockopt( SO_RCVBUF, $buflen );
421             if( not $ok )
422             {
423                 Error('Could not set SO_RCVBUF to ' .
424                       $buflen . ': ' . $!);
425             }
426             else
427             {
428                 Debug('Set SO_RCVBUF to ' . $buflen);
429             }
430         }
431     }
432
433     my $maxmsgsize = $collector->param($token, 'snmp-max-msg-size');
434     if( defined( $maxmsgsize ) and $maxmsgsize > 0 )
435     {
436         $session->max_msg_size( $maxmsgsize );
437         
438     }
439     
440     return $session;
441 }
442
443
444 sub expandOidMappings
445 {
446     my $collector = shift;
447     my $token = shift;
448     my $hosthash = shift;
449     my $oid_in = shift;
450         
451     my $cref = $collector->collectorData( 'snmp' );
452
453     my $oid = $oid_in;
454
455     # Process Map statements
456
457     while( index( $oid, 'M(' ) >= 0 )
458     {
459         if( not $oid =~ /^(.*)M\(\s*([0-9\.]+)\s*,\s*([^\)]+)\)(.*)$/o )
460         {
461             Error("Error in OID mapping syntax: $oid");
462             return undef;
463         }
464
465         my $head = $1;
466         my $map = $2;
467         my $key = $3;
468         my $tail = $4;
469
470         # Remove trailing space from key
471         $key =~ s/\s+$//o;
472
473         my $value =
474             lookupMap( $collector, $token, $hosthash, $map, $key );
475
476         if( defined( $value ) )
477         {
478             if( $value eq 'notfound' )
479             {
480                 return 'notfound';
481             }
482             else
483             {
484                 $oid = $head . $value . $tail;
485             }
486         }
487         else
488         {
489             return undef;
490         }
491     }
492
493     # process value lookups
494
495     while( index( $oid, 'V(' ) >= 0 )
496     {
497         if( not $oid =~ /^(.*)V\(\s*([0-9\.]+)\s*\)(.*)$/o )
498         {
499             Error("Error in OID value lookup syntax: $oid");
500             return undef;
501         }
502
503         my $head = $1;
504         my $key = $2;
505         my $tail = $4;
506
507         my $value;
508
509         if( not defined( $cref->{'value-lookups'}
510                          {$hosthash}{$key} ) )
511         {
512             # Retrieve the OID value from host
513
514             my $session = openBlockingSession( $collector, $token, $hosthash );
515             if( not defined($session) )
516             {
517                 return undef;
518             }
519
520             my $result = $session->get_request( -varbindlist => [$key] );
521             $session->close();
522             if( defined $result and defined($result->{$key}) )
523             {
524                 $value = $result->{$key};
525                 $cref->{'value-lookups'}{$hosthash}{$key} = $value;
526             }
527             else
528             {
529                 Error("Error retrieving $key from $hosthash: " .
530                       $session->error());
531                 probablyDead( $collector, $hosthash );
532                 return undef;
533             }
534         }
535         else
536         {
537             $value =
538                 $cref->{'value-lookups'}{$hosthash}{$key};
539         }
540         if( defined( $value ) )
541         {
542             $oid = $head . $value . $tail;
543         }
544         else
545         {
546             return 'notfound';
547         }
548     }
549
550     # Debug('OID expanded: ' . $oid_in . ' -> ' . $oid');
551     return $oid;
552 }
553
554 # Look up table index in a map by value
555
556 sub lookupMap
557 {
558     my $collector = shift;
559     my $token = shift;
560     my $hosthash = shift;
561     my $map = shift;
562     my $key = shift;
563
564     my $cref = $collector->collectorData( 'snmp' );
565     my $maphash = join('#', $hosthash, $map);
566     
567     if( not defined( $maps{$hosthash}{$map} ) )
568     {
569         my $ret;
570
571         if( defined( $oldMaps{$hosthash}{$map} ) and
572             defined( $key ) )
573         {
574             $ret = $oldMaps{$hosthash}{$map}{$key};
575         }
576         
577         if( $mapLookupScheduled{$maphash} )
578         {
579             return $ret;
580         }
581
582         if( scalar(@mappingSessions) >=
583             $Torrus::Collector::SNMP::maxSessionsPerDispatcher )
584         {
585             snmp_dispatcher();
586             @mappingSessions = ();
587             %mapLookupScheduled = ();
588         }
589
590         # Retrieve map from host
591         Debug('Retrieving map ' . $map . ' from ' . $hosthash);
592
593         my $session = openNonblockingSession( $collector, $token, $hosthash );
594         if( not defined($session) )
595         {
596             return $ret;
597         }
598         else
599         {
600             push( @mappingSessions, $session );
601         }
602
603         # Retrieve the map table
604
605         $session->get_table( -baseoid => $map,
606                              -callback => [\&mapLookupCallback,
607                                            $collector, $hosthash, $map] );
608
609         $mapLookupScheduled{$maphash} = 1;
610
611         if( not $snmpV1Hosts{$hosthash} )
612         {
613             $mapsExpire{$maphash} =
614                 int( time() + $mapsRefreshPeriod +
615                      rand( $mapsRefreshPeriod * $mapsRefreshRandom ) );
616         }
617         
618         return $ret;
619     }
620
621     if( defined( $key ) )
622     {
623         my $value = $maps{$hosthash}{$map}{$key};
624         if( not defined $value )
625         {
626             Error("Cannot find value $key in map $map for $hosthash in ".
627                   $collector->path($token));
628             if( defined ( $maps{$hosthash}{$map} ) )
629             {
630                 Error("Current map follows");
631                 while( my($key, $val) = each
632                        %{$maps{$hosthash}{$map}} )
633                 {
634                     Error("'$key' => '$val'");
635                 }
636             }
637             return 'notfound';
638         }
639         else
640         {
641             if( not $snmpV1Hosts{$hosthash} )
642             {
643                 $cref->{'mapsDependentTokens'}{$maphash}{$token} = 1;
644                 $cref->{'mapsRelatedMaps'}{$token}{$maphash} = 1;
645             }
646             
647             return $value;
648         }
649     }
650     else
651     {
652         return undef;
653     }
654 }
655
656
657 sub mapLookupCallback
658 {
659     my $session = shift;
660     my $collector = shift;
661     my $hosthash = shift;
662     my $map = shift;
663
664     &Torrus::DB::checkInterrupted();
665     
666     Debug('Received mapping PDU from ' . $hosthash);
667
668     my $result = $session->var_bind_list();
669     if( defined $result )
670     {
671         my $preflen = length($map) + 1;
672         
673         while( my( $oid, $key ) = each %{$result} )
674         {
675             my $val = substr($oid, $preflen);
676             $maps{$hosthash}{$map}{$key} = $val;
677             # Debug("Map $map discovered: '$key' -> '$val'");
678         }
679     }
680     else
681     {
682         Error("Error retrieving table $map from $hosthash: " .
683               $session->error());
684         $session->close();
685         probablyDead( $collector, $hosthash );
686         return undef;
687     }    
688 }
689
690 sub activeMappingSessions
691 {
692     return scalar( @mappingSessions );
693 }
694     
695 # The target host is unreachable. We try to reach it few more times and
696 # give it the final diagnose.
697
698 sub probablyDead
699 {
700     my $collector = shift;
701     my $hosthash = shift;
702
703     my $cref = $collector->collectorData( 'snmp' );
704
705     # Stop all collection for this host, until next initTargetAttributes
706     # is successful
707     delete $cref->{'activehosts'}{$hosthash};
708
709     my $probablyAlive = 1;
710
711     if( defined( $hostUnreachableSeen{$hosthash} ) )
712     {
713         if( $Torrus::Collector::SNMP::unreachableTimeout > 0 and
714             time() -
715             $hostUnreachableSeen{$hosthash} >
716             $Torrus::Collector::SNMP::unreachableTimeout )
717         {
718             $probablyAlive = 0;
719         }
720     }
721     else
722     {
723         $hostUnreachableSeen{$hosthash} = time();
724
725         if( defined( $db_failures ) )
726         {
727             $db_failures->host_failure('unreachable', $hosthash);
728             $db_failures->set_counter('unreachable',
729                                       scalar( keys %hostUnreachableSeen));
730         }
731     }
732
733     if( $probablyAlive )
734     {
735         Info('Target host is unreachable. Will try again later: ' . $hosthash);
736     }
737     else
738     {
739         # It is dead indeed. Delete all tokens associated with this host
740         Info('Target host is unreachable during last ' .
741              $Torrus::Collector::SNMP::unreachableTimeout .
742              ' seconds. Giving it up: ' . $hosthash);
743         my @deleteTargets = ();
744         while( my ($oid, $ref1) =
745                each %{$cref->{'targets'}{$hosthash}} )
746         {
747             while( my ($token, $dummy) = each %{$ref1} )
748             {
749                 push( @deleteTargets, $token );
750             }
751         }
752         
753         Debug('Deleting ' . scalar( @deleteTargets ) . ' tokens');
754         foreach my $token ( @deleteTargets )
755         {
756             $collector->deleteTarget($token);
757         }
758                 
759         delete $hostUnreachableSeen{$hosthash};
760         delete $hostUnreachableRetry{$hosthash};
761         $unreachableHostDeleted{$hosthash} = 1;
762
763         if( defined( $db_failures ) )
764         {
765             $db_failures->host_failure('deleted', $hosthash);
766             $db_failures->set_counter('unreachable',
767                                       scalar( keys %hostUnreachableSeen));
768             $db_failures->set_counter('deleted',
769                                       scalar( keys %unreachableHostDeleted));
770         }
771     }
772     
773     return $probablyAlive;
774 }
775
776 # Return false if the try is too early
777
778 sub checkUnreachableRetry
779 {
780     my $collector = shift;
781     my $hosthash = shift;
782
783     my $cref = $collector->collectorData( 'snmp' );
784
785     my $ret = 1;
786     if( $hostUnreachableSeen{$hosthash} )
787     {
788         my $lastRetry = $hostUnreachableRetry{$hosthash};
789
790         if( not defined( $lastRetry ) )
791         {
792             $lastRetry = $hostUnreachableSeen{$hosthash};
793         }
794             
795         if( time() < $lastRetry +
796             $Torrus::Collector::SNMP::unreachableRetryDelay )
797         {
798             $ret = 0;
799         }
800         else
801         {
802             $hostUnreachableRetry{$hosthash} = time();
803         }            
804     }
805     
806     return $ret;
807 }
808
809
810 sub isHostDead
811 {
812     my $collector = shift;
813     my $hosthash = shift;
814
815     my $cref = $collector->collectorData( 'snmp' );
816     return $unreachableHostDeleted{$hosthash};
817 }
818
819
820 sub hostReachableAgain
821 {
822     my $collector = shift;
823     my $hosthash = shift;
824     
825     my $cref = $collector->collectorData( 'snmp' );
826     if( exists( $hostUnreachableSeen{$hosthash} ) )
827     {
828         delete $hostUnreachableSeen{$hosthash};
829         if( defined( $db_failures ) )
830         {
831             $db_failures->remove_host($hosthash);            
832             $db_failures->set_counter('unreachable',
833                                       scalar( keys %hostUnreachableSeen));
834         }
835     }
836 }
837
838
839 # Callback executed by Collector
840
841 sub deleteTarget
842 {
843     my $collector = shift;
844     my $token = shift;
845
846     my $tref = $collector->tokenData( $token );
847     my $cref = $collector->collectorData( 'snmp' );
848
849     my $hosthash = $tref->{'hosthash'};    
850     my $oid = $tref->{'oid'};
851
852     delete $cref->{'targets'}{$hosthash}{$oid}{$token};
853     if( not %{$cref->{'targets'}{$hosthash}{$oid}} )
854     {
855         delete $cref->{'targets'}{$hosthash}{$oid};
856
857         if( not %{$cref->{'targets'}{$hosthash}} )
858         {
859             delete $cref->{'targets'}{$hosthash};
860         }
861     }
862
863     delete $cref->{'needsRemapping'}{$token};
864     
865     foreach my $maphash ( keys %{$cref->{'mapsRelatedMaps'}{$token}} )
866     {
867         delete $cref->{'mapsDependentTokens'}{$maphash}{$token};
868     }
869     delete $cref->{'mapsRelatedMaps'}{$token};
870 }
871
872 # Main collector cycle
873
874 $Torrus::Collector::runCollector{'snmp'} =
875     \&Torrus::Collector::SNMP::runCollector;
876
877 sub runCollector
878 {
879     my $collector = shift;
880     my $cref = shift;
881
882     # Info(sprintf('runCollector() Offset: %d, active hosts: %d, maps: %d',
883     #              $collector->offset(),
884     #              scalar( keys %{$cref->{'activehosts'}} ),
885     #              scalar(keys %maps)));
886     
887     # Create one SNMP session per host address.
888     # We assume that version, timeout and retries are the same
889     # within one address
890
891     # We limit the number of sessions per snmp_dispatcher run
892     # because of some strange bugs: with more than 400 sessions per
893     # dispatcher, some requests are not sent out
894
895     my @hosts = keys %{$cref->{'activehosts'}};
896     
897     while( scalar(@mappingSessions) + scalar(@hosts) > 0 )
898     {
899         my @batch = ();
900         while( ( scalar(@mappingSessions) + scalar(@batch) <
901                  $Torrus::Collector::SNMP::maxSessionsPerDispatcher )
902                and
903                scalar(@hosts) > 0 )
904         {
905             push( @batch, pop( @hosts ) );
906         }
907
908         &Torrus::DB::checkInterrupted();
909
910         my @sessions;
911
912         foreach my $hosthash ( @batch )
913         {
914             my @oids = sort keys %{$cref->{'targets'}{$hosthash}};
915
916             # Info(sprintf('Host %s: %d OIDs',
917             #              $hosthash,
918             #              scalar(@oids)));
919             
920             # Find one representative token for the host
921             
922             if( scalar( @oids ) == 0 )
923             {
924                 next;
925             }
926         
927             my @reptokens = keys %{$cref->{'targets'}{$hosthash}{$oids[0]}};
928             if( scalar( @reptokens ) == 0 )
929             {
930                 next;
931             }
932             my $reptoken = $reptokens[0];
933             
934             my $session =
935                 openNonblockingSession( $collector, $reptoken, $hosthash );
936             
937             &Torrus::DB::checkInterrupted();
938             
939             if( not defined($session) )
940             {
941                 next;
942             }
943             else
944             {
945                 Debug('Created SNMP session for ' . $hosthash);
946                 push( @sessions, $session );
947             }
948             
949             my $oids_per_pdu = $cref->{'oids_per_pdu'}{$hosthash};
950
951             my @pdu_oids = ();
952             my $delay = 0;
953             
954             while( scalar( @oids ) > 0 )
955             {
956                 my $oid = shift @oids;
957                 push( @pdu_oids, $oid );
958
959                 if( scalar( @oids ) == 0 or
960                     ( scalar( @pdu_oids ) >= $oids_per_pdu ) )
961                 {
962                     if( not $cref->{'nosysuptime'}{$hosthash} )
963                     {
964                         # We insert sysUpTime into every PDU, because
965                         # we need it in further processing
966                         push( @pdu_oids, $sysUpTime );
967                     }
968                     
969                     if( Torrus::Log::isDebug() )
970                     {
971                         Debug('Sending SNMP PDU to ' . $hosthash . ':');
972                         foreach my $oid ( @pdu_oids )
973                         {
974                             Debug($oid);
975                         }
976                     }
977
978                     # Generate the list of tokens that form this PDU
979                     my $pdu_tokens = {};
980                     foreach my $oid ( @pdu_oids )
981                     {
982                         if( defined( $cref->{'targets'}{$hosthash}{$oid} ) )
983                         {
984                             foreach my $token
985                                 ( keys %{$cref->{'targets'}{$hosthash}{$oid}} )
986                             {
987                                 $pdu_tokens->{$oid}{$token} = 1;
988                             }
989                         }
990                     }
991                     my $result =
992                         $session->
993                         get_request( -delay => $delay,
994                                      -callback =>
995                                      [ \&Torrus::Collector::SNMP::callback,
996                                        $collector, $pdu_tokens, $hosthash ],
997                                      -varbindlist => \@pdu_oids );
998                     if( not defined $result )
999                     {
1000                         Error("Cannot create SNMP request: " .
1001                               $session->error);
1002                     }
1003                     @pdu_oids = ();
1004                     $delay += 0.01;
1005                 }
1006             }
1007         }
1008         
1009         &Torrus::DB::checkInterrupted();
1010         
1011         snmp_dispatcher();
1012
1013         # Check if there were pending map lookup sessions
1014         
1015         if( scalar( @mappingSessions ) > 0 )
1016         {
1017             @mappingSessions = ();
1018             %mapLookupScheduled = ();
1019         }
1020     }
1021 }
1022
1023
1024 sub callback
1025 {
1026     my $session = shift;
1027     my $collector = shift;
1028     my $pdu_tokens = shift;
1029     my $hosthash = shift;
1030
1031     &Torrus::DB::checkInterrupted();
1032     
1033     my $cref = $collector->collectorData( 'snmp' );
1034
1035     Debug('SNMP Callback executed for ' . $hosthash);
1036
1037     if( not defined( $session->var_bind_list() ) )
1038     {
1039         Error('SNMP Error for ' . $hosthash . ': ' . $session->error() .
1040               ' when retrieving ' . join(' ', sort keys %{$pdu_tokens}));
1041
1042         probablyDead( $collector, $hosthash );
1043         
1044         # Clear the mapping
1045         delete $maps{$hosthash};
1046         foreach my $oid ( keys %{$pdu_tokens} )
1047         {
1048             foreach my $token ( keys %{$pdu_tokens->{$oid}} )
1049             {
1050                 $cref->{'needsRemapping'}{$token} = 1;
1051             }
1052         }
1053         return;
1054     }
1055     else
1056     {
1057         hostReachableAgain( $collector, $hosthash );
1058     }
1059
1060     my $timestamp = time();
1061
1062     my $checkUptime = not $cref->{'nosysuptime'}{$hosthash};
1063     my $doSetValue = 1;
1064     
1065     my $uptime = 0;
1066
1067     if( $checkUptime )
1068     {
1069         my $uptimeTicks = $session->var_bind_list()->{$sysUpTime};
1070         if( defined $uptimeTicks )
1071         {
1072             $uptime = $uptimeTicks / 100;
1073             Debug('Uptime: ' . $uptime);
1074         }
1075         else
1076         {
1077             Error('Did not receive sysUpTime for ' . $hosthash);
1078         }
1079
1080         if( $uptime < $collector->period() or
1081             ( defined($cref->{'knownUptime'}{$hosthash})
1082               and
1083               $uptime + $collector->period() <
1084               $cref->{'knownUptime'}{$hosthash} ) )
1085         {
1086             # The agent has reloaded. Clean all maps and push UNDEF
1087             # values to the storage
1088             
1089             Info('Agent rebooted: ' . $hosthash);
1090             delete $maps{$hosthash};
1091
1092             $timestamp -= $uptime;
1093             foreach my $oid ( keys %{$pdu_tokens} )
1094             {
1095                 foreach my $token ( keys %{$pdu_tokens->{$oid}} )
1096                 {
1097                     $collector->setValue( $token, 'U', $timestamp, $uptime );
1098                     $cref->{'needsRemapping'}{$token} = 1;
1099                 }
1100             }
1101             
1102             $doSetValue = 0;
1103         }
1104         $cref->{'knownUptime'}{$hosthash} = $uptime;
1105     }
1106     
1107     if( $doSetValue )
1108     {
1109         while( my ($oid, $value) = each %{ $session->var_bind_list() } )
1110         {
1111             # Debug("OID=$oid, VAL=$value");
1112             if( $value eq 'noSuchObject' or
1113                 $value eq 'noSuchInstance' or
1114                 $value eq 'endOfMibView' )
1115             {
1116                 if( not $cref->{'ignoremiberrors'}{$hosthash}{$oid} )
1117                 {
1118                     Error("Error retrieving $oid from $hosthash: $value");
1119                     
1120                     foreach my $token ( keys %{$pdu_tokens->{$oid}} )
1121                     {
1122                         if( defined( $db_failures ) )
1123                         {
1124                             $db_failures->mib_error
1125                                 ($hosthash, $collector->path($token));
1126                         }
1127
1128                         $collector->deleteTarget($token);
1129                     }
1130                 }
1131             }
1132             else
1133             {
1134                 if( $cref->{'64bit_oid'}{$oid} )
1135                 {
1136                     $value = Math::BigInt->new($value);
1137                 }
1138
1139                 foreach my $token ( keys %{$pdu_tokens->{$oid}} )
1140                 {
1141                     $collector->setValue( $token, $value,
1142                                           $timestamp, $uptime );
1143                 }
1144             }
1145         }
1146     }
1147 }
1148
1149
1150 # Execute this after the collector has finished
1151
1152 $Torrus::Collector::postProcess{'snmp'} =
1153     \&Torrus::Collector::SNMP::postProcess;
1154
1155 sub postProcess
1156 {
1157     my $collector = shift;
1158     my $cref = shift;
1159
1160     # It could happen that postProcess is called for a collector which
1161     # has no targets, and therefore it's the only place where we can
1162     # initialize these variables
1163     
1164     if( not defined( $cref->{'mapsLastExpireChecked'} ) )
1165     {
1166         $cref->{'mapsLastExpireChecked'} = 0;
1167     }
1168
1169     if( not defined( $cref->{'mapsRefreshed'} ) )
1170     {
1171         $cref->{'mapsRefreshed'} = [];
1172     }
1173     
1174     # look if some maps are ready after last expiration check
1175     if( scalar( @{$cref->{'mapsRefreshed'}} ) > 0 )
1176     {
1177         foreach my $maphash ( @{$cref->{'mapsRefreshed'}} )
1178         {
1179             foreach my $token
1180                 ( keys %{$cref->{'mapsDependentTokens'}{$maphash}} )
1181             {
1182                 $cref->{'needsRemapping'}{$token} = 1;
1183             }
1184         }
1185         $cref->{'mapsRefreshed'} = [];
1186     }
1187
1188     my $now = time();
1189     
1190     if( $cref->{'mapsLastExpireChecked'} + $mapsExpireCheckPeriod <= $now )
1191     {
1192         $cref->{'mapsLastExpireChecked'} = $now;
1193         
1194         # Check the maps expiration and arrange lookup for expired
1195         
1196         while( my ( $maphash, $expire ) = each %mapsExpire )
1197         {
1198             if( $expire <= $now and not $mapLookupScheduled{$maphash} )
1199             {
1200                 &Torrus::DB::checkInterrupted();
1201
1202                 my ( $hosthash, $map ) = split( /\#/o, $maphash );
1203
1204                 if( $unreachableHostDeleted{$hosthash} )
1205                 {
1206                     # This host is no longer polled. Remove the leftovers
1207                     
1208                     delete $mapsExpire{$maphash};
1209                     delete $maps{$hosthash};
1210                 }
1211                 else
1212                 {
1213                     # Find one representative token for the map
1214                     my @tokens =
1215                         keys %{$cref->{'mapsDependentTokens'}{$maphash}};
1216                     if( scalar( @tokens ) == 0 )
1217                     {
1218                         next;
1219                     }
1220                     my $reptoken = $tokens[0];
1221
1222                     # save the map for the time of refresh                    
1223                     $oldMaps{$hosthash}{$map} = $maps{$hosthash}{$map};
1224                     delete $maps{$hosthash}{$map};
1225
1226                     # this will schedule the map retrieval for the next
1227                     # collector cycle
1228                     Debug('Refreshing map: ' . $maphash);
1229                 
1230                     lookupMap( $collector, $reptoken,
1231                                $hosthash, $map, undef );
1232
1233                     # After the next collector period, the maps will be
1234                     # ready and tokens may be updated without losing the data
1235                     push( @{$cref->{'mapsRefreshed'}}, $maphash );
1236                 }
1237             }                
1238         }
1239     }
1240     
1241     foreach my $token ( keys %{$cref->{'needsRemapping'}} )
1242     {
1243         &Torrus::DB::checkInterrupted();
1244
1245         delete $cref->{'needsRemapping'}{$token};
1246         if( not Torrus::Collector::SNMP::initTargetAttributes
1247             ( $collector, $token ) )
1248         {
1249             $collector->deleteTarget($token);
1250         }
1251     }    
1252 }
1253
1254 1;
1255
1256
1257 # Local Variables:
1258 # mode: perl
1259 # indent-tabs-mode: nil
1260 # perl-indent-level: 4
1261 # End: