--- /dev/null
+# Copyright (C) 2002-2007 Stanislav Sinyagin
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
+
+# $Id: SNMP.pm,v 1.1 2010-12-27 00:03:58 ivan Exp $
+# Stanislav Sinyagin <ssinyagin@yahoo.com>
+
+package Torrus::Collector::SNMP;
+
+use Torrus::Collector::SNMP_Params;
+use Torrus::ConfigTree;
+use Torrus::Log;
+use Torrus::SNMP_Failures;
+
+use strict;
+use Net::hostent;
+use Socket;
+use Net::SNMP qw(:snmp);
+use Math::BigInt;
+
+
+# Register the collector type
+$Torrus::Collector::collectorTypes{'snmp'} = 1;
+
+
+# List of needed parameters and default values
+
+$Torrus::Collector::params{'snmp'} = {
+ 'snmp-ipversion' => undef,
+ 'snmp-transport' => undef,
+ 'snmp-version' => undef,
+ 'snmp-port' => undef,
+ 'snmp-community' => undef,
+ 'snmp-username' => undef,
+ 'snmp-authkey' => undef,
+ 'snmp-authpassword' => undef,
+ 'snmp-authprotocol' => 'md5',
+ 'snmp-privkey' => undef,
+ 'snmp-privpassword' => undef,
+ 'snmp-privprotocol' => 'des',
+ 'snmp-timeout' => undef,
+ 'snmp-retries' => undef,
+ 'domain-name' => undef,
+ 'snmp-host' => undef,
+ 'snmp-localaddr' => undef,
+ 'snmp-localport' => undef,
+ 'snmp-object' => undef,
+ 'snmp-oids-per-pdu' => undef,
+ 'snmp-object-type' => 'OTHER',
+ 'snmp-check-sysuptime' => 'yes',
+ 'snmp-max-msg-size' => undef,
+ 'snmp-ignore-mib-errors' => undef,
+ };
+
+my $sysUpTime = '1.3.6.1.2.1.1.3.0';
+
+# Hosts that are running SNMPv1. We do not reresh maps on them, as
+# they are too slow
+my %snmpV1Hosts;
+
+# SNMP tables lookup maps
+my %maps;
+
+# Old lookup maps, used temporarily during refresh cycle
+my %oldMaps;
+
+# How frequent we refresh the SNMP mapping
+our $mapsRefreshPeriod;
+
+# Random factor in refresh period
+our $mapsRefreshRandom;
+
+# Time period after configuration re-compile when we refresh existing mappings
+our $mapsUpdateInterval;
+
+# how often we check for expired maps
+our $mapsExpireCheckPeriod;
+
+# expiration time for each map
+my %mapsExpire;
+
+# Lookups scheduled for execution
+my %mapLookupScheduled;
+
+# SNMP session objects for map lookups
+my @mappingSessions;
+
+
+# Timestamps of hosts last found unreachable
+my %hostUnreachableSeen;
+
+# Last time we tried to reach an unreachable host
+my %hostUnreachableRetry;
+
+# Hosts that were deleted because of unreachability for too long
+my %unreachableHostDeleted;
+
+
+our $db_failures;
+
+# Flush stats after a restart or recompile
+$Torrus::Collector::initCollectorGlobals{'snmp'} =
+ \&Torrus::Collector::SNMP::initCollectorGlobals;
+
+sub initCollectorGlobals
+{
+ my $tree = shift;
+ my $instance = shift;
+
+ if( not defined( $db_failures ) )
+ {
+ $db_failures =
+ new Torrus::SNMP_Failures( -Tree => $tree,
+ -Instance => $instance,
+ -WriteAccess => 1 );
+ }
+
+ if( defined( $db_failures ) )
+ {
+ $db_failures->init();
+ }
+
+ # re-init counters and collect garbage
+ %oldMaps = ();
+ %hostUnreachableSeen = ();
+ %hostUnreachableRetry = ();
+ %unreachableHostDeleted = ();
+
+ # Configuration re-compile was probably caused by new object instances
+ # appearing on the monitored devices. Here we force the maps to refresh
+ # soon enough in order to catch up with the changes
+
+ my $now = time();
+ foreach my $maphash ( keys %mapsExpire )
+ {
+ $mapsExpire{$maphash} = int( $now + rand( $mapsUpdateInterval ) );
+ }
+}
+
+
+# This is first executed per target
+
+$Torrus::Collector::initTarget{'snmp'} = \&Torrus::Collector::SNMP::initTarget;
+
+
+
+sub initTarget
+{
+ my $collector = shift;
+ my $token = shift;
+
+ my $tref = $collector->tokenData( $token );
+ my $cref = $collector->collectorData( 'snmp' );
+
+ $collector->registerDeleteCallback
+ ( $token, \&Torrus::Collector::SNMP::deleteTarget );
+
+ my $hostname = getHostname( $collector, $token );
+ if( not defined( $hostname ) )
+ {
+ return 0;
+ }
+
+ $tref->{'hostname'} = $hostname;
+
+ return Torrus::Collector::SNMP::initTargetAttributes( $collector, $token );
+}
+
+
+sub initTargetAttributes
+{
+ my $collector = shift;
+ my $token = shift;
+
+ &Torrus::DB::checkInterrupted();
+
+ my $tref = $collector->tokenData( $token );
+ my $cref = $collector->collectorData( 'snmp' );
+
+ my $hostname = $tref->{'hostname'};
+ my $port = $collector->param($token, 'snmp-port');
+ my $version = $collector->param($token, 'snmp-version');
+
+ my $community;
+ if( $version eq '1' or $version eq '2c' )
+ {
+ $community = $collector->param($token, 'snmp-community');
+ }
+ else
+ {
+ # We use community string to identify the agent.
+ # For SNMPv3, it's the user name
+ $community = $collector->param($token, 'snmp-username');
+ }
+
+ my $hosthash = join('|', $hostname, $port, $community);
+ $tref->{'hosthash'} = $hosthash;
+
+ if( $version eq '1' )
+ {
+ $snmpV1Hosts{$hosthash} = 1;
+ }
+
+ # If the object is defined as a map, retrieve the whole map
+ # and cache it.
+
+ if( isHostDead( $collector, $hosthash ) )
+ {
+ return 0;
+ }
+
+ if( not checkUnreachableRetry( $collector, $hosthash ) )
+ {
+ $cref->{'needsRemapping'}{$token} = 1;
+ return 1;
+ }
+
+ my $oid = $collector->param($token, 'snmp-object');
+ $oid = expandOidMappings( $collector, $token, $hosthash, $oid );
+
+ if( not $oid )
+ {
+ if( $unreachableHostDeleted{$hosthash} )
+ {
+ # we tried our best, but the target is dead
+ return 0;
+ }
+ else
+ {
+ # we return OK status, to let the storage initiate
+ $cref->{'needsRemapping'}{$token} = 1;
+ return 1;
+ }
+ }
+ elsif( $oid eq 'notfound' )
+ {
+ return 0;
+ }
+
+ # Collector should be able to find the target
+ # by host, port, community, and oid.
+ # There can be several targets with the same host|port|community+oid set.
+
+ $cref->{'targets'}{$hosthash}{$oid}{$token} = 1;
+ $cref->{'activehosts'}{$hosthash} = 1;
+
+ $tref->{'oid'} = $oid;
+
+ $cref->{'oids_per_pdu'}{$hosthash} =
+ $collector->param($token, 'snmp-oids-per-pdu');
+
+ if( $collector->param($token, 'snmp-object-type') eq 'COUNTER64' )
+ {
+ $cref->{'64bit_oid'}{$oid} = 1;
+ }
+
+ if( $collector->param($token, 'snmp-check-sysuptime') eq 'no' )
+ {
+ $cref->{'nosysuptime'}{$hosthash} = 1;
+ }
+
+ if( $collector->param($token, 'snmp-ignore-mib-errors') eq 'yes' )
+ {
+ $cref->{'ignoremiberrors'}{$hosthash}{$oid} = 1;
+ }
+
+ return 1;
+}
+
+
+sub getHostname
+{
+ my $collector = shift;
+ my $token = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+
+ my $hostname = $collector->param($token, 'snmp-host');
+ my $domain = $collector->param($token, 'domain-name');
+
+ if( length( $domain ) > 0 and
+ index($hostname, '.') < 0 and
+ index($hostname, ':') < 0 )
+ {
+ $hostname .= '.' . $domain;
+ }
+
+ return $hostname;
+}
+
+
+sub snmpSessionArgs
+{
+ my $collector = shift;
+ my $token = shift;
+ my $hosthash = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+ if( defined( $cref->{'snmpargs'}{$hosthash} ) )
+ {
+ return $cref->{'snmpargs'}{$hosthash};
+ }
+
+ my $transport = $collector->param($token, 'snmp-transport') . '/ipv' .
+ $collector->param($token, 'snmp-ipversion');
+
+ my ($hostname, $port, $community) = split(/\|/o, $hosthash);
+
+ my $version = $collector->param($token, 'snmp-version');
+ my $ret = [ -domain => $transport,
+ -hostname => $hostname,
+ -port => $port,
+ -timeout => $collector->param($token, 'snmp-timeout'),
+ -retries => $collector->param($token, 'snmp-retries'),
+ -version => $version ];
+
+ foreach my $arg ( qw(-localaddr -localport) )
+ {
+ if( defined( $collector->param($token, 'snmp' . $arg) ) )
+ {
+ push( @{$ret}, $arg, $collector->param($token, 'snmp' . $arg) );
+ }
+ }
+
+ if( $version eq '1' or $version eq '2c' )
+ {
+ push( @{$ret}, '-community', $community );
+ }
+ else
+ {
+ push( @{$ret}, -username, $community);
+
+ foreach my $arg ( qw(-authkey -authpassword -authprotocol
+ -privkey -privpassword -privprotocol) )
+ {
+ if( defined( $collector->param($token, 'snmp' . $arg) ) )
+ {
+ push( @{$ret},
+ $arg, $collector->param($token, 'snmp' . $arg) );
+ }
+ }
+ }
+
+ $cref->{'snmpargs'}{$hosthash} = $ret;
+ return $ret;
+}
+
+
+
+sub openBlockingSession
+{
+ my $collector = shift;
+ my $token = shift;
+ my $hosthash = shift;
+
+ my $args = snmpSessionArgs( $collector, $token, $hosthash );
+ my ($session, $error) =
+ Net::SNMP->session( @{$args},
+ -nonblocking => 0,
+ -translate => ['-all', 0, '-octetstring', 1] );
+ if( not defined($session) )
+ {
+ Error('Cannot create SNMP session for ' . $hosthash . ': ' . $error);
+ }
+ else
+ {
+ my $maxmsgsize = $collector->param($token, 'snmp-max-msg-size');
+ if( defined( $maxmsgsize ) and $maxmsgsize > 0 )
+ {
+ $session->max_msg_size( $maxmsgsize );
+ }
+ }
+
+ return $session;
+}
+
+sub openNonblockingSession
+{
+ my $collector = shift;
+ my $token = shift;
+ my $hosthash = shift;
+
+ my $args = snmpSessionArgs( $collector, $token, $hosthash );
+
+ my ($session, $error) =
+ Net::SNMP->session( @{$args},
+ -nonblocking => 0x1,
+ -translate => ['-timeticks' => 0] );
+ if( not defined($session) )
+ {
+ Error('Cannot create SNMP session for ' . $hosthash . ': ' . $error);
+ return undef;
+ }
+
+ if( $collector->param($token, 'snmp-transport') eq 'udp' )
+ {
+ # We set SO_RCVBUF only once, because Net::SNMP shares
+ # one UDP socket for all sessions.
+
+ my $sock_name = $session->transport()->sock_name();
+ my $refcount = $Net::SNMP::Transport::SOCKETS->{
+ $sock_name}->[&Net::SNMP::Transport::_SHARED_REFC()];
+
+ if( $refcount == 1 )
+ {
+ my $buflen = int($Torrus::Collector::SNMP::RxBuffer);
+ my $socket = $session->transport()->socket();
+ my $ok = $socket->sockopt( SO_RCVBUF, $buflen );
+ if( not $ok )
+ {
+ Error('Could not set SO_RCVBUF to ' .
+ $buflen . ': ' . $!);
+ }
+ else
+ {
+ Debug('Set SO_RCVBUF to ' . $buflen);
+ }
+ }
+ }
+
+ my $maxmsgsize = $collector->param($token, 'snmp-max-msg-size');
+ if( defined( $maxmsgsize ) and $maxmsgsize > 0 )
+ {
+ $session->max_msg_size( $maxmsgsize );
+
+ }
+
+ return $session;
+}
+
+
+sub expandOidMappings
+{
+ my $collector = shift;
+ my $token = shift;
+ my $hosthash = shift;
+ my $oid_in = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+
+ my $oid = $oid_in;
+
+ # Process Map statements
+
+ while( index( $oid, 'M(' ) >= 0 )
+ {
+ if( not $oid =~ /^(.*)M\(\s*([0-9\.]+)\s*,\s*([^\)]+)\)(.*)$/o )
+ {
+ Error("Error in OID mapping syntax: $oid");
+ return undef;
+ }
+
+ my $head = $1;
+ my $map = $2;
+ my $key = $3;
+ my $tail = $4;
+
+ # Remove trailing space from key
+ $key =~ s/\s+$//o;
+
+ my $value =
+ lookupMap( $collector, $token, $hosthash, $map, $key );
+
+ if( defined( $value ) )
+ {
+ if( $value eq 'notfound' )
+ {
+ return 'notfound';
+ }
+ else
+ {
+ $oid = $head . $value . $tail;
+ }
+ }
+ else
+ {
+ return undef;
+ }
+ }
+
+ # process value lookups
+
+ while( index( $oid, 'V(' ) >= 0 )
+ {
+ if( not $oid =~ /^(.*)V\(\s*([0-9\.]+)\s*\)(.*)$/o )
+ {
+ Error("Error in OID value lookup syntax: $oid");
+ return undef;
+ }
+
+ my $head = $1;
+ my $key = $2;
+ my $tail = $4;
+
+ my $value;
+
+ if( not defined( $cref->{'value-lookups'}
+ {$hosthash}{$key} ) )
+ {
+ # Retrieve the OID value from host
+
+ my $session = openBlockingSession( $collector, $token, $hosthash );
+ if( not defined($session) )
+ {
+ return undef;
+ }
+
+ my $result = $session->get_request( -varbindlist => [$key] );
+ $session->close();
+ if( defined $result and defined($result->{$key}) )
+ {
+ $value = $result->{$key};
+ $cref->{'value-lookups'}{$hosthash}{$key} = $value;
+ }
+ else
+ {
+ Error("Error retrieving $key from $hosthash: " .
+ $session->error());
+ probablyDead( $collector, $hosthash );
+ return undef;
+ }
+ }
+ else
+ {
+ $value =
+ $cref->{'value-lookups'}{$hosthash}{$key};
+ }
+ if( defined( $value ) )
+ {
+ $oid = $head . $value . $tail;
+ }
+ else
+ {
+ return 'notfound';
+ }
+ }
+
+ # Debug('OID expanded: ' . $oid_in . ' -> ' . $oid');
+ return $oid;
+}
+
+# Look up table index in a map by value
+
+sub lookupMap
+{
+ my $collector = shift;
+ my $token = shift;
+ my $hosthash = shift;
+ my $map = shift;
+ my $key = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+ my $maphash = join('#', $hosthash, $map);
+
+ if( not defined( $maps{$hosthash}{$map} ) )
+ {
+ my $ret;
+
+ if( defined( $oldMaps{$hosthash}{$map} ) and
+ defined( $key ) )
+ {
+ $ret = $oldMaps{$hosthash}{$map}{$key};
+ }
+
+ if( $mapLookupScheduled{$maphash} )
+ {
+ return $ret;
+ }
+
+ if( scalar(@mappingSessions) >=
+ $Torrus::Collector::SNMP::maxSessionsPerDispatcher )
+ {
+ snmp_dispatcher();
+ @mappingSessions = ();
+ %mapLookupScheduled = ();
+ }
+
+ # Retrieve map from host
+ Debug('Retrieving map ' . $map . ' from ' . $hosthash);
+
+ my $session = openNonblockingSession( $collector, $token, $hosthash );
+ if( not defined($session) )
+ {
+ return $ret;
+ }
+ else
+ {
+ push( @mappingSessions, $session );
+ }
+
+ # Retrieve the map table
+
+ $session->get_table( -baseoid => $map,
+ -callback => [\&mapLookupCallback,
+ $collector, $hosthash, $map] );
+
+ $mapLookupScheduled{$maphash} = 1;
+
+ if( not $snmpV1Hosts{$hosthash} )
+ {
+ $mapsExpire{$maphash} =
+ int( time() + $mapsRefreshPeriod +
+ rand( $mapsRefreshPeriod * $mapsRefreshRandom ) );
+ }
+
+ return $ret;
+ }
+
+ if( defined( $key ) )
+ {
+ my $value = $maps{$hosthash}{$map}{$key};
+ if( not defined $value )
+ {
+ Error("Cannot find value $key in map $map for $hosthash in ".
+ $collector->path($token));
+ if( defined ( $maps{$hosthash}{$map} ) )
+ {
+ Error("Current map follows");
+ while( my($key, $val) = each
+ %{$maps{$hosthash}{$map}} )
+ {
+ Error("'$key' => '$val'");
+ }
+ }
+ return 'notfound';
+ }
+ else
+ {
+ if( not $snmpV1Hosts{$hosthash} )
+ {
+ $cref->{'mapsDependentTokens'}{$maphash}{$token} = 1;
+ $cref->{'mapsRelatedMaps'}{$token}{$maphash} = 1;
+ }
+
+ return $value;
+ }
+ }
+ else
+ {
+ return undef;
+ }
+}
+
+
+sub mapLookupCallback
+{
+ my $session = shift;
+ my $collector = shift;
+ my $hosthash = shift;
+ my $map = shift;
+
+ &Torrus::DB::checkInterrupted();
+
+ Debug('Received mapping PDU from ' . $hosthash);
+
+ my $result = $session->var_bind_list();
+ if( defined $result )
+ {
+ my $preflen = length($map) + 1;
+
+ while( my( $oid, $key ) = each %{$result} )
+ {
+ my $val = substr($oid, $preflen);
+ $maps{$hosthash}{$map}{$key} = $val;
+ # Debug("Map $map discovered: '$key' -> '$val'");
+ }
+ }
+ else
+ {
+ Error("Error retrieving table $map from $hosthash: " .
+ $session->error());
+ $session->close();
+ probablyDead( $collector, $hosthash );
+ return undef;
+ }
+}
+
+sub activeMappingSessions
+{
+ return scalar( @mappingSessions );
+}
+
+# The target host is unreachable. We try to reach it few more times and
+# give it the final diagnose.
+
+sub probablyDead
+{
+ my $collector = shift;
+ my $hosthash = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+
+ # Stop all collection for this host, until next initTargetAttributes
+ # is successful
+ delete $cref->{'activehosts'}{$hosthash};
+
+ my $probablyAlive = 1;
+
+ if( defined( $hostUnreachableSeen{$hosthash} ) )
+ {
+ if( $Torrus::Collector::SNMP::unreachableTimeout > 0 and
+ time() -
+ $hostUnreachableSeen{$hosthash} >
+ $Torrus::Collector::SNMP::unreachableTimeout )
+ {
+ $probablyAlive = 0;
+ }
+ }
+ else
+ {
+ $hostUnreachableSeen{$hosthash} = time();
+
+ if( defined( $db_failures ) )
+ {
+ $db_failures->host_failure('unreachable', $hosthash);
+ $db_failures->set_counter('unreachable',
+ scalar( keys %hostUnreachableSeen));
+ }
+ }
+
+ if( $probablyAlive )
+ {
+ Info('Target host is unreachable. Will try again later: ' . $hosthash);
+ }
+ else
+ {
+ # It is dead indeed. Delete all tokens associated with this host
+ Info('Target host is unreachable during last ' .
+ $Torrus::Collector::SNMP::unreachableTimeout .
+ ' seconds. Giving it up: ' . $hosthash);
+ my @deleteTargets = ();
+ while( my ($oid, $ref1) =
+ each %{$cref->{'targets'}{$hosthash}} )
+ {
+ while( my ($token, $dummy) = each %{$ref1} )
+ {
+ push( @deleteTargets, $token );
+ }
+ }
+
+ Debug('Deleting ' . scalar( @deleteTargets ) . ' tokens');
+ foreach my $token ( @deleteTargets )
+ {
+ $collector->deleteTarget($token);
+ }
+
+ delete $hostUnreachableSeen{$hosthash};
+ delete $hostUnreachableRetry{$hosthash};
+ $unreachableHostDeleted{$hosthash} = 1;
+
+ if( defined( $db_failures ) )
+ {
+ $db_failures->host_failure('deleted', $hosthash);
+ $db_failures->set_counter('unreachable',
+ scalar( keys %hostUnreachableSeen));
+ $db_failures->set_counter('deleted',
+ scalar( keys %unreachableHostDeleted));
+ }
+ }
+
+ return $probablyAlive;
+}
+
+# Return false if the try is too early
+
+sub checkUnreachableRetry
+{
+ my $collector = shift;
+ my $hosthash = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+
+ my $ret = 1;
+ if( $hostUnreachableSeen{$hosthash} )
+ {
+ my $lastRetry = $hostUnreachableRetry{$hosthash};
+
+ if( not defined( $lastRetry ) )
+ {
+ $lastRetry = $hostUnreachableSeen{$hosthash};
+ }
+
+ if( time() < $lastRetry +
+ $Torrus::Collector::SNMP::unreachableRetryDelay )
+ {
+ $ret = 0;
+ }
+ else
+ {
+ $hostUnreachableRetry{$hosthash} = time();
+ }
+ }
+
+ return $ret;
+}
+
+
+sub isHostDead
+{
+ my $collector = shift;
+ my $hosthash = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+ return $unreachableHostDeleted{$hosthash};
+}
+
+
+sub hostReachableAgain
+{
+ my $collector = shift;
+ my $hosthash = shift;
+
+ my $cref = $collector->collectorData( 'snmp' );
+ if( exists( $hostUnreachableSeen{$hosthash} ) )
+ {
+ delete $hostUnreachableSeen{$hosthash};
+ if( defined( $db_failures ) )
+ {
+ $db_failures->remove_host($hosthash);
+ $db_failures->set_counter('unreachable',
+ scalar( keys %hostUnreachableSeen));
+ }
+ }
+}
+
+
+# Callback executed by Collector
+
+sub deleteTarget
+{
+ my $collector = shift;
+ my $token = shift;
+
+ my $tref = $collector->tokenData( $token );
+ my $cref = $collector->collectorData( 'snmp' );
+
+ my $hosthash = $tref->{'hosthash'};
+ my $oid = $tref->{'oid'};
+
+ delete $cref->{'targets'}{$hosthash}{$oid}{$token};
+ if( not %{$cref->{'targets'}{$hosthash}{$oid}} )
+ {
+ delete $cref->{'targets'}{$hosthash}{$oid};
+
+ if( not %{$cref->{'targets'}{$hosthash}} )
+ {
+ delete $cref->{'targets'}{$hosthash};
+ }
+ }
+
+ delete $cref->{'needsRemapping'}{$token};
+
+ foreach my $maphash ( keys %{$cref->{'mapsRelatedMaps'}{$token}} )
+ {
+ delete $cref->{'mapsDependentTokens'}{$maphash}{$token};
+ }
+ delete $cref->{'mapsRelatedMaps'}{$token};
+}
+
+# Main collector cycle
+
+$Torrus::Collector::runCollector{'snmp'} =
+ \&Torrus::Collector::SNMP::runCollector;
+
+sub runCollector
+{
+ my $collector = shift;
+ my $cref = shift;
+
+ # Info(sprintf('runCollector() Offset: %d, active hosts: %d, maps: %d',
+ # $collector->offset(),
+ # scalar( keys %{$cref->{'activehosts'}} ),
+ # scalar(keys %maps)));
+
+ # Create one SNMP session per host address.
+ # We assume that version, timeout and retries are the same
+ # within one address
+
+ # We limit the number of sessions per snmp_dispatcher run
+ # because of some strange bugs: with more than 400 sessions per
+ # dispatcher, some requests are not sent out
+
+ my @hosts = keys %{$cref->{'activehosts'}};
+
+ while( scalar(@mappingSessions) + scalar(@hosts) > 0 )
+ {
+ my @batch = ();
+ while( ( scalar(@mappingSessions) + scalar(@batch) <
+ $Torrus::Collector::SNMP::maxSessionsPerDispatcher )
+ and
+ scalar(@hosts) > 0 )
+ {
+ push( @batch, pop( @hosts ) );
+ }
+
+ &Torrus::DB::checkInterrupted();
+
+ my @sessions;
+
+ foreach my $hosthash ( @batch )
+ {
+ my @oids = sort keys %{$cref->{'targets'}{$hosthash}};
+
+ # Info(sprintf('Host %s: %d OIDs',
+ # $hosthash,
+ # scalar(@oids)));
+
+ # Find one representative token for the host
+
+ if( scalar( @oids ) == 0 )
+ {
+ next;
+ }
+
+ my @reptokens = keys %{$cref->{'targets'}{$hosthash}{$oids[0]}};
+ if( scalar( @reptokens ) == 0 )
+ {
+ next;
+ }
+ my $reptoken = $reptokens[0];
+
+ my $session =
+ openNonblockingSession( $collector, $reptoken, $hosthash );
+
+ &Torrus::DB::checkInterrupted();
+
+ if( not defined($session) )
+ {
+ next;
+ }
+ else
+ {
+ Debug('Created SNMP session for ' . $hosthash);
+ push( @sessions, $session );
+ }
+
+ my $oids_per_pdu = $cref->{'oids_per_pdu'}{$hosthash};
+
+ my @pdu_oids = ();
+ my $delay = 0;
+
+ while( scalar( @oids ) > 0 )
+ {
+ my $oid = shift @oids;
+ push( @pdu_oids, $oid );
+
+ if( scalar( @oids ) == 0 or
+ ( scalar( @pdu_oids ) >= $oids_per_pdu ) )
+ {
+ if( not $cref->{'nosysuptime'}{$hosthash} )
+ {
+ # We insert sysUpTime into every PDU, because
+ # we need it in further processing
+ push( @pdu_oids, $sysUpTime );
+ }
+
+ if( Torrus::Log::isDebug() )
+ {
+ Debug('Sending SNMP PDU to ' . $hosthash . ':');
+ foreach my $oid ( @pdu_oids )
+ {
+ Debug($oid);
+ }
+ }
+
+ # Generate the list of tokens that form this PDU
+ my $pdu_tokens = {};
+ foreach my $oid ( @pdu_oids )
+ {
+ if( defined( $cref->{'targets'}{$hosthash}{$oid} ) )
+ {
+ foreach my $token
+ ( keys %{$cref->{'targets'}{$hosthash}{$oid}} )
+ {
+ $pdu_tokens->{$oid}{$token} = 1;
+ }
+ }
+ }
+ my $result =
+ $session->
+ get_request( -delay => $delay,
+ -callback =>
+ [ \&Torrus::Collector::SNMP::callback,
+ $collector, $pdu_tokens, $hosthash ],
+ -varbindlist => \@pdu_oids );
+ if( not defined $result )
+ {
+ Error("Cannot create SNMP request: " .
+ $session->error);
+ }
+ @pdu_oids = ();
+ $delay += 0.01;
+ }
+ }
+ }
+
+ &Torrus::DB::checkInterrupted();
+
+ snmp_dispatcher();
+
+ # Check if there were pending map lookup sessions
+
+ if( scalar( @mappingSessions ) > 0 )
+ {
+ @mappingSessions = ();
+ %mapLookupScheduled = ();
+ }
+ }
+}
+
+
+sub callback
+{
+ my $session = shift;
+ my $collector = shift;
+ my $pdu_tokens = shift;
+ my $hosthash = shift;
+
+ &Torrus::DB::checkInterrupted();
+
+ my $cref = $collector->collectorData( 'snmp' );
+
+ Debug('SNMP Callback executed for ' . $hosthash);
+
+ if( not defined( $session->var_bind_list() ) )
+ {
+ Error('SNMP Error for ' . $hosthash . ': ' . $session->error() .
+ ' when retrieving ' . join(' ', sort keys %{$pdu_tokens}));
+
+ probablyDead( $collector, $hosthash );
+
+ # Clear the mapping
+ delete $maps{$hosthash};
+ foreach my $oid ( keys %{$pdu_tokens} )
+ {
+ foreach my $token ( keys %{$pdu_tokens->{$oid}} )
+ {
+ $cref->{'needsRemapping'}{$token} = 1;
+ }
+ }
+ return;
+ }
+ else
+ {
+ hostReachableAgain( $collector, $hosthash );
+ }
+
+ my $timestamp = time();
+
+ my $checkUptime = not $cref->{'nosysuptime'}{$hosthash};
+ my $doSetValue = 1;
+
+ my $uptime = 0;
+
+ if( $checkUptime )
+ {
+ my $uptimeTicks = $session->var_bind_list()->{$sysUpTime};
+ if( defined $uptimeTicks )
+ {
+ $uptime = $uptimeTicks / 100;
+ Debug('Uptime: ' . $uptime);
+ }
+ else
+ {
+ Error('Did not receive sysUpTime for ' . $hosthash);
+ }
+
+ if( $uptime < $collector->period() or
+ ( defined($cref->{'knownUptime'}{$hosthash})
+ and
+ $uptime + $collector->period() <
+ $cref->{'knownUptime'}{$hosthash} ) )
+ {
+ # The agent has reloaded. Clean all maps and push UNDEF
+ # values to the storage
+
+ Info('Agent rebooted: ' . $hosthash);
+ delete $maps{$hosthash};
+
+ $timestamp -= $uptime;
+ foreach my $oid ( keys %{$pdu_tokens} )
+ {
+ foreach my $token ( keys %{$pdu_tokens->{$oid}} )
+ {
+ $collector->setValue( $token, 'U', $timestamp, $uptime );
+ $cref->{'needsRemapping'}{$token} = 1;
+ }
+ }
+
+ $doSetValue = 0;
+ }
+ $cref->{'knownUptime'}{$hosthash} = $uptime;
+ }
+
+ if( $doSetValue )
+ {
+ while( my ($oid, $value) = each %{ $session->var_bind_list() } )
+ {
+ # Debug("OID=$oid, VAL=$value");
+ if( $value eq 'noSuchObject' or
+ $value eq 'noSuchInstance' or
+ $value eq 'endOfMibView' )
+ {
+ if( not $cref->{'ignoremiberrors'}{$hosthash}{$oid} )
+ {
+ Error("Error retrieving $oid from $hosthash: $value");
+
+ foreach my $token ( keys %{$pdu_tokens->{$oid}} )
+ {
+ if( defined( $db_failures ) )
+ {
+ $db_failures->mib_error
+ ($hosthash, $collector->path($token));
+ }
+
+ $collector->deleteTarget($token);
+ }
+ }
+ }
+ else
+ {
+ if( $cref->{'64bit_oid'}{$oid} )
+ {
+ $value = Math::BigInt->new($value);
+ }
+
+ foreach my $token ( keys %{$pdu_tokens->{$oid}} )
+ {
+ $collector->setValue( $token, $value,
+ $timestamp, $uptime );
+ }
+ }
+ }
+ }
+}
+
+
+# Execute this after the collector has finished
+
+$Torrus::Collector::postProcess{'snmp'} =
+ \&Torrus::Collector::SNMP::postProcess;
+
+sub postProcess
+{
+ my $collector = shift;
+ my $cref = shift;
+
+ # It could happen that postProcess is called for a collector which
+ # has no targets, and therefore it's the only place where we can
+ # initialize these variables
+
+ if( not defined( $cref->{'mapsLastExpireChecked'} ) )
+ {
+ $cref->{'mapsLastExpireChecked'} = 0;
+ }
+
+ if( not defined( $cref->{'mapsRefreshed'} ) )
+ {
+ $cref->{'mapsRefreshed'} = [];
+ }
+
+ # look if some maps are ready after last expiration check
+ if( scalar( @{$cref->{'mapsRefreshed'}} ) > 0 )
+ {
+ foreach my $maphash ( @{$cref->{'mapsRefreshed'}} )
+ {
+ foreach my $token
+ ( keys %{$cref->{'mapsDependentTokens'}{$maphash}} )
+ {
+ $cref->{'needsRemapping'}{$token} = 1;
+ }
+ }
+ $cref->{'mapsRefreshed'} = [];
+ }
+
+ my $now = time();
+
+ if( $cref->{'mapsLastExpireChecked'} + $mapsExpireCheckPeriod <= $now )
+ {
+ $cref->{'mapsLastExpireChecked'} = $now;
+
+ # Check the maps expiration and arrange lookup for expired
+
+ while( my ( $maphash, $expire ) = each %mapsExpire )
+ {
+ if( $expire <= $now and not $mapLookupScheduled{$maphash} )
+ {
+ &Torrus::DB::checkInterrupted();
+
+ my ( $hosthash, $map ) = split( /\#/o, $maphash );
+
+ if( $unreachableHostDeleted{$hosthash} )
+ {
+ # This host is no longer polled. Remove the leftovers
+
+ delete $mapsExpire{$maphash};
+ delete $maps{$hosthash};
+ }
+ else
+ {
+ # Find one representative token for the map
+ my @tokens =
+ keys %{$cref->{'mapsDependentTokens'}{$maphash}};
+ if( scalar( @tokens ) == 0 )
+ {
+ next;
+ }
+ my $reptoken = $tokens[0];
+
+ # save the map for the time of refresh
+ $oldMaps{$hosthash}{$map} = $maps{$hosthash}{$map};
+ delete $maps{$hosthash}{$map};
+
+ # this will schedule the map retrieval for the next
+ # collector cycle
+ Debug('Refreshing map: ' . $maphash);
+
+ lookupMap( $collector, $reptoken,
+ $hosthash, $map, undef );
+
+ # After the next collector period, the maps will be
+ # ready and tokens may be updated without losing the data
+ push( @{$cref->{'mapsRefreshed'}}, $maphash );
+ }
+ }
+ }
+ }
+
+ foreach my $token ( keys %{$cref->{'needsRemapping'}} )
+ {
+ &Torrus::DB::checkInterrupted();
+
+ delete $cref->{'needsRemapping'}{$token};
+ if( not Torrus::Collector::SNMP::initTargetAttributes
+ ( $collector, $token ) )
+ {
+ $collector->deleteTarget($token);
+ }
+ }
+}
+
+1;
+
+
+# Local Variables:
+# mode: perl
+# indent-tabs-mode: nil
+# perl-indent-level: 4
+# End: