1 # Copyright (C) 2002-2010 Stanislav Sinyagin
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.
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.
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.
17 # $Id: DevDiscover.pm,v 1.1 2010-12-27 00:03:43 ivan Exp $
18 # Stanislav Sinyagin <ssinyagin@yahoo.com>
20 # Core SNMP device discovery module
22 package Torrus::DevDiscover::DevDetails;
24 package Torrus::DevDiscover;
27 use POSIX qw(strftime);
28 use Net::SNMP qw(:snmp :asn1);
29 use Digest::MD5 qw(md5);
35 foreach my $mod ( @Torrus::DevDiscover::loadModules )
37 eval( 'require ' . $mod );
42 # Custom overlays for templates
44 # 'Module::templateName' -> { 'name' => 'templateName',
45 # 'source' => 'filename.xml' }
46 our %templateOverlays;
60 $defaultParams{'rrd-hwpredict'} = 'no';
61 $defaultParams{'domain-name'} = '';
62 $defaultParams{'host-subtree'} = '';
63 $defaultParams{'snmp-check-sysuptime'} = 'yes';
64 $defaultParams{'show-recursive'} = 'yes';
65 $defaultParams{'snmp-ipversion'} = '4';
66 $defaultParams{'snmp-transport'} = 'udp';
70 'collector-timeoffset',
71 'collector-dispersed-timeoffset',
72 'collector-timeoffset-min',
73 'collector-timeoffset-max',
74 'collector-timeoffset-step',
99 'snmp-check-sysuptime',
104 %Torrus::DevDiscover::oiddef =
106 'system' => '1.3.6.1.2.1.1',
107 'sysDescr' => '1.3.6.1.2.1.1.1.0',
108 'sysObjectID' => '1.3.6.1.2.1.1.2.0',
109 'sysUpTime' => '1.3.6.1.2.1.1.3.0',
110 'sysContact' => '1.3.6.1.2.1.1.4.0',
111 'sysName' => '1.3.6.1.2.1.1.5.0',
112 'sysLocation' => '1.3.6.1.2.1.1.6.0'
115 my @systemOIDs = ('sysDescr', 'sysObjectID', 'sysUpTime', 'sysContact',
116 'sysName', 'sysLocation');
125 $self->{'oiddef'} = {};
126 $self->{'oidrev'} = {};
128 # Combine all %MODULE::oiddef hashes into one
129 foreach my $module ( 'Torrus::DevDiscover',
130 @Torrus::DevDiscover::loadModules )
132 while( my($name, $oid) = each %{eval('\%'.$module.'::oiddef')} )
135 $self->{'oiddef'}->{$name} = $oid;
136 $self->{'oidrev'}->{$oid} = $name;
140 $self->{'datadirs'} = {};
141 $self->{'globalData'} = {};
151 return $self->{'globalData'};
158 my @paramhashes = @_;
160 my $devdetails = new Torrus::DevDiscover::DevDetails();
162 foreach my $params ( \%defaultParams, @paramhashes )
164 $devdetails->setParams( $params );
167 foreach my $param ( @requiredParams )
169 if( not defined( $devdetails->param( $param ) ) )
171 Error('Required parameter not defined: ' . $param);
179 my $version = $devdetails->param( 'snmp-version' );
180 $snmpargs{'-version'} = $version;
182 foreach my $arg ( qw(-port -localaddr -localport -timeout -retries) )
184 if( defined( $devdetails->param( 'snmp' . $arg ) ) )
186 $snmpargs{$arg} = $devdetails->param( 'snmp' . $arg );
190 $snmpargs{'-domain'} = $devdetails->param('snmp-transport') . '/ipv' .
191 $devdetails->param('snmp-ipversion');
193 if( $version eq '1' or $version eq '2c' )
195 $community = $devdetails->param( 'snmp-community' );
196 if( not defined( $community ) )
198 Error('Required parameter not defined: snmp-community');
201 $snmpargs{'-community'} = $community;
203 # set maxMsgSize to a maximum value for better compatibility
205 my $maxmsgsize = $devdetails->param('snmp-max-msg-size');
206 if( defined( $maxmsgsize ) )
208 $devdetails->setParam('snmp-max-msg-size', $maxmsgsize);
209 $snmpargs{'-maxmsgsize'} = $maxmsgsize;
212 elsif( $version eq '3' )
214 foreach my $arg ( qw(-username -authkey -authpassword -authprotocol
215 -privkey -privpassword -privprotocol) )
217 if( defined $devdetails->param( 'snmp' . $arg ) )
219 $snmpargs{$arg} = $devdetails->param( 'snmp' . $arg );
222 $community = $snmpargs{'-username'};
223 if( not defined( $community ) )
225 Error('Required parameter not defined: snmp-user');
231 Error('Illegal value for snmp-version parameter: ' . $version);
235 my $hostname = $devdetails->param('snmp-host');
236 my $domain = $devdetails->param('domain-name');
238 if( $domain and index($hostname, '.') < 0 and index($hostname, ':') < 0 )
240 $hostname .= '.' . $domain;
242 $snmpargs{'-hostname'} = $hostname;
244 my $port = $snmpargs{'-port'};
245 Debug('Discovering host: ' . $hostname . ':' . $port . ':' . $community);
247 my ($session, $error) =
248 Net::SNMP->session( %snmpargs,
250 -translate => ['-all', 0, '-octetstring', 1] );
251 if( not defined($session) )
253 Error('Cannot create SNMP session: ' . $error);
258 foreach my $var ( @systemOIDs )
260 push( @oids, $self->oiddef( $var ) );
263 # This is the only checking if the remote agent is alive
265 my $result = $session->get_request( -varbindlist => \@oids );
266 if( defined $result )
268 $devdetails->storeSnmpVars( $result );
272 # When the remote agent is reacheable, but system objecs are
273 # not implemented, we get a positive error_status
274 if( $session->error_status() == 0 )
276 Error("Unable to communicate with SNMP agent on " . $hostname .
277 ':' . $port . ':' . $community . " - " . $session->error());
282 my $data = $devdetails->data();
283 $data->{'param'} = {};
285 $data->{'templates'} = [];
286 my $customTmpl = $devdetails->param('custom-host-templates');
287 if( length( $customTmpl ) > 0 )
289 push( @{$data->{'templates'}}, split( /\s*,\s*/, $customTmpl ) );
292 # Build host-level legend
296 'name' => 'Location',
297 'value' => $devdetails->snmpVar($self->oiddef('sysLocation'))
301 'value' => $devdetails->snmpVar($self->oiddef('sysContact'))
304 'name' => 'System ID',
305 'value' => $devdetails->param('system-id')
308 'name' => 'Description',
309 'value' => $devdetails->snmpVar($self->oiddef('sysDescr'))
313 if( defined( $devdetails->snmpVar($self->oiddef('sysUpTime')) ) )
315 $legendValues{40}{'name'} = 'Uptime';
316 $legendValues{40}{'value'} =
317 sprintf("%d days since %s",
318 $devdetails->snmpVar($self->oiddef('sysUpTime')) /
320 strftime($Torrus::DevDiscover::timeFormat,
325 foreach my $key ( sort keys %legendValues )
327 my $text = $legendValues{$key}{'value'};
328 if( length( $text ) > 0 )
330 $text = $devdetails->screenSpecialChars( $text );
331 $legend .= $legendValues{$key}{'name'} . ':' . $text . ';';
335 if( $devdetails->param('suppress-legend') ne 'yes' )
337 $data->{'param'}{'legend'} = $legend;
340 # some parameters need just one-to-one copying
343 split('\s*,\s*', $devdetails->param('host-copy-params'));
345 foreach my $param ( @copyParams, @hostCopyParams )
347 my $val = $devdetails->param( $param );
348 if( length( $val ) > 0 )
350 $data->{'param'}{$param} = $val;
354 # If snmp-host is ipv6 address, system-id needs to be adapted to
357 if( not defined( $data->{'param'}{'system-id'} ) and
358 index($data->{'param'}{'snmp-host'}, ':') >= 0 )
360 my $systemid = $data->{'param'}{'snmp-host'};
361 $systemid =~ s/:/_/g;
362 $data->{'param'}{'system-id'} = $systemid;
365 if( not defined( $devdetails->snmpVar($self->oiddef('sysUpTime')) ) )
367 Debug('Agent does not support sysUpTime');
368 $data->{'param'}{'snmp-check-sysuptime'} = 'no';
371 $data->{'param'}{'data-dir'} =
372 $self->genDataDir( $devdetails->param('data-dir'), $hostname );
374 # Register the directory for listDataDirs()
375 $self->{'datadirs'}{$devdetails->param('data-dir')} = 1;
377 $self->{'session'} = $session;
379 # some discovery modules need to be disabled on per-device basis
382 my $useOnlyDevtypes = 0;
383 foreach my $devtype ( split('\s*,\s*',
384 $devdetails->param('only-devtypes') ) )
386 $onlyDevtypes{$devtype} = 1;
387 $useOnlyDevtypes = 1;
390 my %disabledDevtypes;
391 foreach my $devtype ( split('\s*,\s*',
392 $devdetails->param('disable-devtypes') ) )
394 $disabledDevtypes{$devtype} = 1;
397 # 'checkdevtype' procedures for each known device type return true
398 # when it's their device. They also research the device capabilities.
399 my $reg = \%Torrus::DevDiscover::registry;
401 ( sort {$reg->{$a}{'sequence'} <=> $reg->{$b}{'sequence'}}
404 if( ( not $useOnlyDevtypes or $onlyDevtypes{$devtype} ) and
405 not $disabledDevtypes{$devtype} and
406 &{$reg->{$devtype}{'checkdevtype'}}($self, $devdetails) )
408 $devdetails->setDevType( $devtype );
409 Debug('Found device type: ' . $devtype);
413 my @devtypes = sort {
414 $reg->{$a}{'sequence'} <=> $reg->{$b}{'sequence'}
415 } $devdetails->getDevTypes();
416 $data->{'param'}{'devdiscover-devtypes'} = join(',', @devtypes);
418 $data->{'param'}{'devdiscover-nodetype'} = '::device';
420 # Do the detailed discovery and prepare data
422 foreach my $devtype ( @devtypes )
424 $ok = &{$reg->{$devtype}{'discover'}}($self, $devdetails) ? $ok:0;
427 delete $self->{'session'};
430 $devdetails->applySelectors();
432 my $subtree = $devdetails->param('host-subtree');
433 if( not defined( $self->{'devdetails'}{$subtree} ) )
435 $self->{'devdetails'}{$subtree} = [];
437 push( @{$self->{'devdetails'}{$subtree}}, $devdetails );
439 my $define_tokensets = $devdetails->param('define-tokensets');
440 if( defined( $define_tokensets ) and length( $define_tokensets ) > 0 )
442 foreach my $pair ( split(/\s*;\s*/, $define_tokensets ) )
444 my( $tset, $description ) = split( /\s*:\s*/, $pair );
445 if( $tset !~ /^[a-z][a-z0-9-_]*$/ )
447 Error('Invalid name for tokenset: ' . $tset);
450 elsif( length( $description ) == 0 )
452 Error('Missing description for tokenset: ' . $tset);
457 $self->{'define-tokensets'}{$tset} = $description;
470 my $reg = \%Torrus::DevDiscover::registry;
472 foreach my $subtree ( sort keys %{$self->{'devdetails'}} )
474 # Chop the first and last slashes
479 # generate subtree path XML
480 my $subtreeNode = undef;
481 foreach my $subtreeName ( split( '/', $path ) )
483 $subtreeNode = $cb->addSubtree( $subtreeNode, $subtreeName );
486 foreach my $devdetails
487 ( sort {$a->param('snmp-host') cmp $b->param('snmp-host')}
488 @{$self->{'devdetails'}{$subtree}} )
491 my $data = $devdetails->data();
493 my @registryOverlays = ();
494 if( defined( $devdetails->param('template-registry-overlays' ) ) )
498 $devdetails->param('template-registry-overlays' ));
499 foreach my $overlayName ( @overlayNames )
501 if( defined( $templateOverlays{$overlayName}) )
503 push( @registryOverlays,
504 $templateOverlays{$overlayName} );
508 Error('Cannot find the template overlay named ' .
514 # we should call this anyway, in order to flush the overlays
515 # set by previous host
516 $cb->setRegistryOverlays( @registryOverlays );
518 if( $devdetails->param('disable-snmpcollector' ) eq 'yes' )
520 push( @{$data->{'templates'}}, '::viewonly-defaults' );
524 push( @{$data->{'templates'}}, '::snmp-defaults' );
527 if( $devdetails->param('rrd-hwpredict' ) eq 'yes' )
529 push( @{$data->{'templates'}}, '::holt-winters-defaults' );
533 my $devNodeName = $devdetails->param('symbolic-name');
534 if( length( $devNodeName ) == 0 )
536 $devNodeName = $devdetails->param('system-id');
537 if( length( $devNodeName ) == 0 )
539 $devNodeName = $devdetails->param('snmp-host');
543 my $devNode = $cb->addSubtree( $subtreeNode, $devNodeName,
545 $data->{'templates'} );
547 my $aliases = $devdetails->param('host-aliases');
548 if( length( $aliases ) > 0 )
550 foreach my $alias ( split( '\s*,\s*', $aliases ) )
552 $cb->addAlias( $devNode, $alias );
556 my $includeFiles = $devdetails->param('include-files');
557 if( length( $includeFiles ) > 0 )
559 foreach my $file ( split( '\s*,\s*', $includeFiles ) )
561 $cb->addFileInclusion( $file );
566 # Let the device type-specific modules add children
569 ( sort {$reg->{$a}{'sequence'} <=> $reg->{$b}{'sequence'}}
570 $devdetails->getDevTypes() )
572 &{$reg->{$devtype}{'buildConfig'}}
573 ( $devdetails, $cb, $devNode, $self->{'globalData'} );
576 $cb->{'statistics'}{'hosts'}++;
581 ( sort {$reg->{$a}{'sequence'} <=> $reg->{$b}{'sequence'}}
584 if( defined( $reg->{$devtype}{'buildGlobalConfig'} ) )
586 &{$reg->{$devtype}{'buildGlobalConfig'}}($cb,
587 $self->{'globalData'});
591 if( defined( $self->{'define-tokensets'} ) )
593 my $tsetsNode = $cb->startTokensets();
594 foreach my $tset ( sort keys %{$self->{'define-tokensets'}} )
596 $cb->addTokenset( $tsetsNode, $tset, {
597 'comment' => $self->{'define-tokensets'}{$tset} } );
607 return $self->{'session'};
615 my $ret = $self->{'oiddef'}->{$var};
618 Error('Undefined OID definition: ' . $var);
628 return $self->{'oidref'}->{$oid};
636 my $hostname = shift;
638 if( $Torrus::DevDiscover::hashDataDirEnabled )
640 return $basedir . '/' .
641 sprintf( $Torrus::DevDiscover::hashDataDirFormat,
642 unpack('N', md5($hostname)) %
643 $Torrus::DevDiscover::hashDataDirBucketSize );
656 my @basedirs = keys %{$self->{'datadirs'}};
659 if( $Torrus::DevDiscover::hashDataDirEnabled )
661 foreach my $basedir ( @basedirs )
664 $i < $Torrus::DevDiscover::hashDataDirBucketSize;
667 push( @ret, $basedir . '/' .
668 sprintf( $Torrus::DevDiscover::hashDataDirFormat, $i ) );
676 # Check if SNMP table is present, without retrieving the whole table
683 my $session = $self->session();
684 my $oid = $self->oiddef( $oidname );
686 my $result = $session->get_next_request( -varbindlist => [ $oid ] );
687 if( defined( $result ) )
689 # check if the returned oid shares the base of the query
690 my $firstOid = (keys %{$result})[0];
691 if( Net::SNMP::oid_base_match( $oid, $firstOid ) and
692 length( $result->{$firstOid} ) > 0 )
702 # Check if given OID is present
709 my $session = $self->session();
710 my $oid = $self->oiddef( $oidname );
712 my $result = $session->get_request( -varbindlist => [ $oid ] );
713 if( $session->error_status() == 0 and
715 defined($result->{$oid}) and
716 length($result->{$oid}) > 0 )
725 # retrieve the given OIDs by names and return hash with values
732 my $session = $self->session();
734 foreach my $oidname ( @oidnames )
736 push( @{$oids}, $self->oiddef( $oidname ) );
739 my $result = $session->get_request( -varbindlist => $oids );
740 if( $session->error_status() == 0 and defined( $result ) )
743 foreach my $oidname ( @oidnames )
745 $ret->{$oidname} = $result->{$self->oiddef( $oidname )};
753 # Simple wrapper for Net::SNMP::oid_base_match
758 my $base_oid = shift;
761 if( $base_oid =~ /^\D/ )
763 $base_oid = $self->oiddef( $base_oid );
765 return Net::SNMP::oid_base_match( $base_oid, $oid );
769 # some discovery modules need to adjust max-msg-size
774 my $devdetails = shift;
778 $opt = {} unless defined($opt);
780 if( (not $opt->{'only_v1_and_v2'}) or $self->session()->version() != 3 )
782 $self->session()->max_msg_size($msgsize);
783 $devdetails->data()->{'param'}{'snmp-max-msg-size'} = $msgsize;
790 ###########################################################################
791 #### Torrus::DevDiscover::DevDetails: the information container for a device
794 package Torrus::DevDiscover::DevDetails;
806 $self->{'params'} = {};
807 $self->{'snmpvars'} = {}; # SNMP results stored here
808 $self->{'devtype'} = {}; # Device types
809 $self->{'caps'} = {}; # Device capabilities
810 $self->{'data'} = {}; # Discovery data
821 while( my ($param, $value) = each %{$params} )
823 $self->{'params'}->{$param} = $value;
834 $self->{'params'}->{$param} = $value;
842 return $self->{'params'}->{$name};
847 # store the query results for later use
854 while( my( $oid, $value ) = each %{$vars} )
856 if( $oid !~ /^\d[0-9.]+\d$/o )
858 Error("Invalid OID syntax: '$oid'");
862 $self->{'snmpvars'}{$oid} = $value;
864 while( length( $oid ) > 0 )
868 if( not exists( $self->{'snmpvars'}{$oid} ) )
870 $self->{'snmpvars'}{$oid} = undef;
876 # Clean the cache of sorted OIDs
877 $self->{'sortedoids'} = undef;
881 # check if the stored query results have such OID prefix
889 if( exists( $self->{'snmpvars'}{$oid} ) )
897 # get the value of stored SNMP variable
903 return $self->{'snmpvars'}{$oid};
907 # get the list of table indices for the specified prefix
914 # Remember the sorted OIDs, as sorting is quite expensive for large
917 if( not defined( $self->{'sortedoids'} ) )
919 $self->{'sortedoids'} = [];
920 push( @{$self->{'sortedoids'}},
921 Net::SNMP::oid_lex_sort( keys %{$self->{'snmpvars'}} ) );
925 my $prefixLen = length( $prefix ) + 1;
928 foreach my $oid ( @{$self->{'sortedoids'}} )
930 if( defined($self->{'snmpvars'}{$oid} ) )
932 if( Net::SNMP::oid_base_match( $prefix, $oid ) )
934 # Extract the index from OID
935 my $index = substr( $oid, $prefixLen );
936 push( @ret, $index );
950 # device type is the registered discovery module name
956 $self->{'devtype'}{$type} = 1;
963 return $self->{'devtype'}{$type};
969 return keys %{$self->{'devtype'}};
973 # device capabilities. Each discovery module may define its own set of
974 # capabilities and use them for information exchange between checkdevtype(),
975 # discover(), and buildConfig() of its own and dependant modules
981 Debug('Device capability: ' . $cap);
982 $self->{'caps'}{$cap} = 1;
989 return $self->{'caps'}{$cap};
996 Debug('Clearing device capability: ' . $cap);
997 if( exists( $self->{'caps'}{$cap} ) )
999 delete $self->{'caps'}{$cap};
1008 return $self->{'data'};
1012 sub screenSpecialChars
1017 $txt =~ s/:/{COLON}/gm;
1018 $txt =~ s/;/{SEMICOL}/gm;
1019 $txt =~ s/%/{PERCENT}/gm;
1029 my $selList = $self->param('selectors');
1030 return if not defined( $selList );
1032 my $reg = \%Torrus::DevDiscover::selectorsRegistry;
1034 foreach my $sel ( split('\s*,\s*', $selList) )
1036 my $type = $self->param( $sel . '-selector-type' );
1037 if( not defined( $type ) )
1039 Error('Parameter ' . $sel . '-selector-type must be defined ' .
1040 'for ' . $self->param('snmp-host'));
1042 elsif( not exists( $reg->{$type} ) )
1044 Error('Unknown selector type: ' . $type .
1045 ' for ' . $self->param('snmp-host'));
1049 Debug('Initializing selector: ' . $sel);
1051 my $treg = $reg->{$type};
1052 my @objects = &{$treg->{'getObjects'}}( $self, $type );
1054 foreach my $object ( @objects )
1056 Debug('Checking object: ' .
1057 &{$treg->{'getObjectName'}}( $self, $object, $type ));
1059 my $expr = $self->param( $sel . '-selector-expr' );
1060 $expr = '1' if length( $expr ) == 0;
1065 my $checkval = $self->param( $sel . '-' . $attr );
1067 Debug('Checking attribute: ' . $attr .
1068 ' and value: ' . $checkval);
1069 my $ret = &{$treg->{'checkAttribute'}}( $self,
1072 Debug(sprintf('Returned value: %d', $ret));
1076 my $rpn = new Torrus::RPN;
1077 my $result = $rpn->run( $expr, $callback );
1078 Debug('Selector result: ' . $result);
1081 my $actions = $self->param( $sel . '-selector-actions' );
1082 foreach my $action ( split('\s*,\s*', $actions) )
1085 $self->param( $sel . '-' . $action . '-arg' );
1086 $arg = 1 if not defined( $arg );
1088 Debug('Applying action: ' . $action .
1089 ' with argument: ' . $arg);
1090 &{$treg->{'applyAction'}}( $self, $object, $type,
1104 # indent-tabs-mode: nil
1105 # perl-indent-level: 4