#!/usr/local/bin/perl # -*- perl -*- # Cricket: a configuration, polling and data display wrapper for RRD files # # Copyright (C) 1998 Jeff R. Allen and WebTV Networks, Inc. # # 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., 675 Mass Ave, Cambridge, MA 02139, USA. BEGIN { my $programdir = (($0 =~ m:^(.*/):)[0] || "./") . "."; eval "require '$programdir/cricket-conf.pl'"; #eval "require '/usr/local/etc/cricket-conf.pl'" # unless $Common::global::gInstallRoot; $Common::global::gInstallRoot ||= $programdir; } require 5.8.0; use lib "$Common::global::gInstallRoot/lib"; use Threaded; use RRDs 1.000101; use Common::Version; use Common::global; use Common::Options; use Common::Util; use Common::Map; use Common::HandleTarget; use ConfigTree::Cache; use Monitor; # Don't change these here! Set them in the Defaults file. These # should also not be removed because of backwards compatability my $wait_time = 1; my $wait_max = 240; my $max_threads = 100; my $is_threaded; # here's where the individual datasource routines live use snmp; use exec; use file; use field; # Remove the comment on the next line if you want to use sql logging # use sql; if (Common::Util::isWin32()) { eval "use wbem"; eval "use perfmon"; } Info("starting"); $start_time = time; # See the documentation on ds-source FUNC for why this defaults # to commented out. # use func; Common::Options::commonOptions(); Info("Starting collector: $Common::global::gVersion"); $gTargetCt = 0; $Common::global::isCollector = 1; $Common::global::gCT = new ConfigTree::Cache; $gCT = $Common::global::gCT; $gCT->Base($Common::global::gConfigRoot); $gCT->Warn(\&Warn); Info("calling init") if (defined($logging)); if (!$gCT->init()) { Die("Failed to open compiled config tree from " . "$Common::global::gConfigRoot/config.db: $!"); } my($recomp, $why) = $gCT->needsRecompile(); if ($recomp) { Warn("Config tree needs to be recompiled: $why"); system( (Common::Util::isWin32() ? 'perl ' : '') . "$Common::global::gInstallRoot/compile " . "-base $Common::global::gConfigRoot"); $gCT = new ConfigTree::Cache; $gCT->Base($Common::global::gConfigRoot); if (! $gCT->init()) { Die("Failed to open compiled config tree from " . "$Common::global::gConfigRoot/config.db: $!"); } } # if they gave us no subtrees to focus on, use the root of the config tree if ($#ARGV+1 == 0) { push @ARGV, '/'; } # foreach subtree to do # find the base node of that subtree # foreach leaf node of this subtree # process it my($subtree,%targets); foreach $subtree (@ARGV) { if ($gCT->nodeExists($subtree)) { $gCT->visitLeafs($subtree, \&handleTarget, \&handleTargetInstance, \&localHandleTargetInstance); } else { Warn("Unknown subtree $subtree."); } } foreach $subtree (@ARGV) { if ($gCT->nodeExists($subtree)) { $gCT->visitLeafs($subtree, \&handleTarget, \&checkTargetInstance); } else { Warn("Unknown subtree $subtree."); } } # Placeholder for threaded collection $th = 1; #foreach my $name (sort rand_sort keys %targets) { foreach my $name (sort keys %targets) { my(@mixedResults); my $type = $targets{$name}{'type'}; my $target = $targets{$name}{'target'}; my $dsList = $targets{$name}{'dsList'}; Debug("threaded: $target->{'threaded'}"); if(defined $target->{'threaded'} == 1) { $is_threaded = 1; Debug("Threading defined; using threaded collection"); $wait_time = $target->{'threaded_waittime'} if(defined($target->{'threaded_waittime'})); $wait_max = $target->{'threaded_waitmax'} if(defined($target->{'threaded_waitmax'})); $max_threads = $target->{'threaded_maxthreads'} if(defined($target->{'maxthreads'})); Debug("Thread wait_time: $wait_time; wait_max: $wait_max; max_threads = $max_threads"); ThreadedCollect::wait_for_thread($wait_time,$max_threads,$th); Info("Kick off thread"); ThreadedCollect::create_threads($name, $type, $target, $dsList, $th); $th++; } else { $is_threaded = 0; Debug("Threading not defined; using un-threaded collection"); localUpdateRRDDataFile($name,$type,$target,$dsList); } } # Make sure the threads clean themselves up if(defined($th)) { my $diff = time-$start_time; ThreadedCollect::wait_for_complete($wait_max,$max_threads,$wait_time); $diff = time-$start_time; } # print some summary stuff (number of targets, time taken) # before exiting. my($time) = runTime(); Info("Processed $gTargetCt targets in $time."); # only use strict for the subroutines use strict; sub rand_sort { rand() <=> rand (); } sub localUpdateRRDDataFile { my($name,$type,$target,$dsList) = @_; my($tname) = $target->{'auto-target-name'}; my($tpath) = $target->{'auto-target-path'}; my($datafile) = $target->{'rrd-datafile'}; my $agent_restart = 0; my(@data) = retrieveData($name,$type,$target,$dsList,\$agent_restart); if ($#data+1 == 0) { Warn("No data retrieved. Skipping RRD update."); return; } if (!defined($datafile)) { Warn("Could not find a datafile for $tname."); return; } if (! -f $datafile) { return unless newRRD($name, $target); } # look for date strings, suck em out. my($data, $when, @data2); foreach $data (@data) { if ($data =~ /@(\d+)/) { my($when2) = $1; if (defined($when) && ($when ne $when2)) { Warn("Found inconsistent times in retrieved data. " . "Using first one seen."); } else { $when = $when2; } $data =~ s/\@${when2}//; } push @data2, $data; } if (! defined($when)) { # if they didn't tell us when, use RRD's "now" syntax. $when = "N"; } # If an SNMP agent restart occurred, insert a dummy record consisting # of all "U" one second before the current sample. This causes RRD # to set the previous value of counters to undefined, avoiding weird # results when a counter goes negative because of a restart. if ($agent_restart && $when eq "N") { my @dummyresults = @data2; grep { $_ = 'U'} @dummyresults; Info("Inserting dummy record because of agent restart"); my $now = time(); RRDs::update($datafile, join(":", $now - 1, @dummyresults)); } RRDs::update($datafile, join(":", $when, @data2)); if (my $error = RRDs::error()) { Warn("Cannot update $datafile: $error\n"); } # if we were asked to copy this to someplace, go for it. if (defined($target->{'copy-to'})) { my($copyto) = $target->{'copy-to'}; my($to, $args) = split(/:/, $copyto, 2); $to = lc($to); if ($to eq "trap") { # In this case, args is expected to have SNMP info in # it. Example: trap:public@nms-101 # We just pass it straight thru to snmptrap. # This number lets our NMS identify this as a Cricket # data trap. The enterprise for the trap # is hardcoded in snmpUtils::trap. my($specific) = 3; snmpUtils::trap($args, $specific, "/$tpath/$tname", @data2); } elsif ($to eq "sql") { sqlUtils::sendto($args, @data2); } else { Warn("Unknown copy-to type: $to. Ignoring."); } } } sub localHandleTargetInstance { my($name, $target) = @_; my($tname) = $target->{'auto-target-name'}; my($tpath) = $target->{'auto-target-path'}; # first, dump the dict, to help debug things my($k, $v, $t); $t = "target $tname\n"; foreach $k (sort (keys(%{$target}))) { next if ($k eq "auto-target-name"); $v = $target->{$k}; $v = "[undef]" if (! defined($v)); $t .= "\t$k = $v\n"; } Debug($t); # skip this if it's a meta-target if ((defined($target->{'targets'})) || (defined($target->{'mtargets'}))) { Debug("Skipping meta target $tname"); return; } if (defined($target->{'collect'}) && isFalse($target->{'collect'})) { Debug("Skipping target $tname due to collect=false tag."); return; } $main::gTargetCt++; my $agent_restart = 0; my(@data) = retrieveParams($name, $target, \$agent_restart); } sub handleMultiTarget { # the collector completely ignores this -- it's only # used by the grapher. } sub retrieveData { my($name,$type,$target,$dsList,$restart_ref) = @_; my(@mixedResults); my $dsCount = $target->{'dsCount'}; my $tname = $target->{'auto-target-name'}; my $start = time; my %dataLog; my $dataLog; push(@mixedResults, map { $_ = $type . ":" . $_ } &{$main::gDSFetch{$type}}($dsList, $name, $target)); # Reassemble the data in the right order. my($line, @results); foreach $line (@mixedResults) { my($type, $index, $value) = split(/:/, $line, 3); # only take the first token from the line... that # way, they can put in-line comments in the returned data. # do this only for 'exec' and 'file' data sources if (lc($type) ne 'snmp') { $value =~ s/^\s*//; ($value) = split(/\s+/, $value, 2); } $results[$index] = $value; $dataLog{$index} = $value; } my $num_data = @results; my $diff = time - $start; # Make sure there are no gaps in the return data! # If any data is missing, make it undefined ("U") my($ctr, $missingData) = (0, 0); for $ctr (0 .. $#results) { if (! defined $results[$ctr]) { $results[$ctr] = "U"; $missingData = 1; } else { if ($results[$ctr] eq 'U') { $missingData = 1; } } } # Check the agent uptime with the poll interval. If the uptime is # less then one poll interval, notify our caller of the restart. if (defined($target->{'snmp-uptime'})) { my($agent_uptime) = pop @results; my($poll) = $target->{'rrd-poll-interval'}; $poll = 300 unless (defined($poll)); if ($agent_uptime ne "U" && $agent_uptime < $poll * 100) { Info("Agent uptime is less than poll interval"); $$restart_ref = 1 if (defined($restart_ref)); } } # if we are verifying, check the # fetched mapping key to make certain it's right my($mapkey, $mapRef, $match, $snmp, $baseOID, $oid); if (defined($target->{'--verify-mapkey--'})) { $mapkey = $target->{'--verify-mapkey--'}; $mapRef = $main::gCT->configHash($name, 'map', $mapkey, $target); if (defined($mapRef)) { $match = $mapRef->{'match'}; if (defined($match)) { $match = ConfigTree::Cache::expandString($match, $target, \&Warn); } $baseOID = $mapRef->{'base-oid'}; my($oidMap) = $main::gCT->configHash($name, 'oid', undef, $target); $oid = mapOid($oidMap, $baseOID); } else { Warn("Unknown mapkey. This should not happen."); } $snmp = $target->{'snmp'}; if (!defined($match) || !defined($snmp) || !defined($oid)) { Warn("Data needed to verify $mapkey is missing. " . "Skipping verification."); delete($target->{'--verify-mapkey--'}); } else { my($inst) = $target->{'inst'}; } my($fetchedKey) = pop @results; my($wrongInst); if ($match =~ /^\s*\/(.*)\/\s*$/) { $match = $1; $wrongInst = ($fetchedKey !~ /$match/i); } else { $wrongInst = ($fetchedKey ne $match); } if ($wrongInst) { # damn, they didn't match. this means we need to # fix the instance number using mapInstance, and # retry. Common::Map::mapInstance($name, $target); if (defined($target->{'inst'})) { # now that we have the correct inst, fetch again # (this time there is no need to verify) delete($target->{'--verify-mapkey--'}); @results = retrieveData($name, $target, undef); } else { # fill in all unknown, since the mapping key seems # to no longer exist #@results = (); #for ($ctr = 0; $ctr < $dsCount; $ctr++) { # push @results, "U"; #} } } } my($numRes) = $#results+1; if($numRes != $dsCount) { Warn("$dsCount datasources required, $numRes results returned!"); @results = (); } # Beautify the log output foreach my $key (sort {$a <=> $b} keys %dataLog) { my $val = $dataLog{$key}; $val =~ s/^U$/*** U ***/g; $dataLog .= "ds$key: $val, "; } # I'm too lazy to use substr(). Sue me. chop($dataLog); chop($dataLog); Info("Retrieved data for $tname: $dataLog"); Info("Some data is missing for $tname.") if ($missingData); return @results; } sub retrieveParams { my($name, $target, $restart_ref) = @_; my($tname) = $target->{'auto-target-name'}; my($inst) = $target->{'inst'}; if (defined($inst)) { $tname .= " ($inst)"; } my($ttype) = lc($target->{'target-type'}); my($ttRef) = $main::gCT->configHash($name, 'targettype', $ttype, $target); if (! $ttRef) { Warn("Unknown target type: $ttype."); return; } Debug("Retrieving data for target $tname ($ttype)"); # Determine the list of data sources for this target. my(@targetDSs) = split(/\s*,\s*/, $ttRef->{'ds'}); if (!@targetDSs) { Warn("Could not find any datasources for target type $ttype."); return (); } # take the count before we start dicking with it # below... my($dsCount) = $#targetDSs + 1; # if we need to fetch a key to verify a cached instance, # we append it to targetDSs here. my($mapkey, $mapRef, $match, $snmp, $baseOID, $oid); if ($target->{'--verify-mapkey--'}) { $mapkey = $target->{'--verify-mapkey--'}; $mapRef = $main::gCT->configHash($name, 'map', $mapkey, $target); if (defined($mapRef)) { $match = $mapRef->{'match'}; if (defined($match)) { $match = ConfigTree::Cache::expandString($match, $target, \&Warn); } $baseOID = $mapRef->{'base-oid'}; my($oidMap) = $main::gCT->configHash($name, 'oid', undef, $target); $oid = mapOid($oidMap, $baseOID); } else { Warn("Unknown mapkey. This should not happen."); } $snmp = $target->{'snmp'}; if (!defined($match) || !defined($snmp) || !defined($oid)) { Warn("Data needed to verify $mapkey is missing. " . "Skipping verification."); delete($target->{'--verify-mapkey--'}); } else { my($inst) = $target->{'inst'}; my($newds) = "--snmp://${snmp}/${oid}.${inst}"; push @targetDSs, $newds; } } if (defined($target->{'snmp-uptime'})) { my $ds = "--snmp://$target->{'snmp'}/$target->{'snmp-uptime'}"; push @targetDSs, $ds; } # this will hold a hash of ds-method names. the values will # be a ref to a list of the sources that get passed to # the ds-method later. my(%targetDataSources) = (); my($dsIndex) = 0; my($ds); foreach $ds ( @targetDSs ) { my($dataSource); if ($ds =~ /^--/) { # this is a hacked one, from the verify code, above. $dataSource = $ds; $dataSource =~ s/^--//; } else { # this is a normal ds, so go look it up. my($dsRef) = $main::gCT->configHash($name, 'datasource', lc($ds), $target); if ($dsRef) { # computed data sources need to be excluded fetch if ($dsRef->{'rrd-ds-type'} eq 'COMPUTE') { $dsCount--; # don't penalize for missing these DS next; } $dataSource = $dsRef->{'ds-source'}; $dataSource = ConfigTree::Cache::expandString($dataSource, $target, \&Warn); } else { Warn("Datasource named $ds not found."); next; } } my($dsMethod,$dsLine) = split(':', $dataSource, 2); # create a hash entry which is an array of datasources # of the same TYPE (i.e. snmp, shell, etc). # NOTE: The datasource type is REPLACED by the datasource # index. This is so that we can be sure to reassemble the # data source return value array in the right order. push(@{ $targetDataSources{lc($dsMethod)} }, "$dsIndex:$dsLine"); Debug("ds$dsIndex is: $dsLine"); $dsIndex++; } # For each different data source type (snmp, exec, etc.) # call the fetcher to retrieve the data. my($dsList, $type, @mixedResults,$diff,$error); while (($type, $dsList) = each %targetDataSources) { if (defined ($main::gDSFetch{$type})) { my $starttime = time; $targets{$name}{'type'} = $type; $targets{$name}{'target'} = $target; $targets{$name}{'dsList'} = $dsList; $target->{'dsCount'} = $dsCount; Debug("Collecting params for $name"); } else { Warn("Could not find a fetcher with type $type to " . "fetch data for $tname."); } } } sub newRRD { # Create a new RRD file base on the contents of the # referenced dictionary. my($name, $target) = @_; my($datafile) = $target->{'rrd-datafile'}; my($tname) = $target->{'auto-target-name'}; my($ttype) = lc($target->{'target-type'}); # Create the data directory if it does not exist: $datafile =~ /(.*)\/.*$/; my($dataDir) = $1; if (! -d $dataDir) { Common::Util::MkDir($dataDir); } # Start rrd one week ago. my($week) = (60 * 60 * 24 * 7); my($start) = time - $week - 1; my($poll) = $target->{'rrd-poll-interval'}; $poll = 300 unless (defined($poll)); my(@arg) = ($datafile, "--start", $start, "--step", $poll); my($type, $val); my($valid) = 0; my(@dsDesc) = (); my(@rraDesc) = (); my($dsCnt) = 0; my($ttRef) = $main::gCT->configHash($name, 'targettype', $ttype, $target); if (! $ttRef) { Warn("Unknown target type: $ttype."); return; } # first, process the ds tag my($dses) = $ttRef->{'ds'}; if (! $dses) { Warn("No ds tag found for target type $ttype."); return; } my($ds); foreach $ds (split(/\s*,\s*/, $dses)) { my($dsname) = lc($ds); my($d) = $main::gCT->configHash($name, 'datasource', $dsname, $target); if (defined($d)) { my($dst) = $d->{'rrd-ds-type'}; $dst = "GAUGE" unless (defined($dst)); $dst = uc($dst); my($hb) = $d->{'rrd-heartbeat'}; $hb = $target->{'rrd-heartbeat'} if (defined($target->{'rrd-heartbeat'})); $hb = 1800 unless (defined($hb)); my($min) = $d->{'rrd-min'}; $min = $target->{'rrd-min'} if (defined($target->{'rrd-min'})); $min = 'U' unless (defined($min)); my($max) = $d->{'rrd-max'}; $max = $target->{'rrd-max'} if (defined($target->{'rrd-max'})); $max = 'U' unless (defined($max)); my($cdef) = $d -> {'rrd-cdef'}; if ($dst eq 'COMPUTE' && !defined($cdef)) { Warn("Datasource $ds is of type $dst but tag " . "rrd-cdef is missing."); return; } if ($dst ne 'COMPUTE') { push @dsDesc, join(":", 'DS', "ds$dsCnt", $dst, $hb, $min, $max); } else { push @dsDesc, join(":", 'DS', "ds$dsCnt", $dst, $cdef); } $dsCnt++; } else { Warn("Datasource $ds referenced by target " . "type $ttype does not exist."); return; } } # now do RRAs my($rras) = $ttRef->{'rra'}; if (! $rras) { Warn("No rra tag found for target type $ttype."); return; } my($rra); foreach $rra (split(/\s*,\s*/, $rras)) { my($rraname) = lc($rra); my($r) = $main::gCT->configHash($name, 'rra', undef, $target); if (defined($r)) { if (defined $r->{$rraname}) { push(@rraDesc, "RRA:$r->{$rraname}"); } else { Warn("RRA $rra referenced by target " . "type $ttype does not exist."); return; } } else { # this shouldn't happen... right? Warn("Could not find RRA dictionary."); return; } } push(@arg, @dsDesc, @rraDesc); Info("Creating datafile $datafile for target name $tname."); Debug(join(' ', 'RRDs::create', @arg)); RRDs::create(@arg); if (my $error = RRDs::error()) { Warn("Cannot create $datafile: $error\n"); } return 1; } # Local Variables: # mode: perl # indent-tabs-mode: nil # tab-width: 4 # perl-indent-level: 4