#!/usr/local/bin/perl -w
# -*- 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 {
	$gInstallRoot = (($0 =~ m:^(.*/):)[0] || "./") . ".";
}

use lib "$gInstallRoot/lib";

use RRDs;

use ConfigTree;
use common;

# here's where the individual datasource routines live
use snmp;
use exec;
use file;
# See the documentation on ds-source FUNC for why this defaults
# to commented out.
# use func;

# do not auto-convert by default
$gConvert = 0;

commonOptions( "convert!" => \$gConvert );

$gCT = newConfigTree();
$gCT->base($gBase);
 
if ($#ARGV+1 > 0) {
	foreach $focus (@ARGV) {
		$gCT->addFocus($focus);
	}
} else {
	# if they gave no args, then they meant "all"
	$gCT->addFocus($gBase);
}

Info("Starting collector: $gVersion");

$gTargetCt = 0; 
$gCT->processTree();

$time = time() - $^T;
if ($time > 59) {
	my($min) = int($time / 60);
	my($sec) = $time - ($min * 60);
	$time = "$min minutes, $sec";
}
$time .= " seconds";

Info("Processed $gTargetCt targets in $time.");
exit;

# only use strict for the subroutines
use strict;

sub localHandleTargetInstance {
	my($ct, $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;
	}

	$main::gTargetCt++;

	my($datafile) = $target->{'rrd-datafile'};

	if (!defined($datafile)) {
		Warn("Could not find a datafile for $tname.");
		return;
	}

	if (! -f $datafile) {
		return unless newRRD($ct, $target);
	}

	my(@data) = retrieveData($ct, $target);
	if ($#data+1 == 0) {
		Warn("No data retrieved. Skipping RRD update.");
		return;
	}

	# 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 ($main::gConvert && needConvert($datafile)) {
		Info("Converting $datafile to new format.");
		rrdconvert($datafile);
	}

	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);
		} else {
			Warn("Unknown copy-to type: $to. Ignoring.");
		}
	}
}

sub handleMultiTarget {
	# the collector completely ignores this -- it's only
	# used by the grapher.
}

sub validateTType {
	my($ct, $ttype, $tname) = @_;

	if (! defined($ttype) || ! $ttype) {
		Warn("Could not find target-type for target $tname.");
		return;
	}

	if (! defined($ct->cfg()->{'targettype'}->{$ttype})) {
		Warn("Unknown target-type ($ttype) for target $tname.");
		return;
	}

	return 1;
}

# Procedure to retrieve data from all the datasources in the target

sub retrieveData {
    my($ct, $target) = @_;

	my($tname) = $target->{'auto-target-name'};
	my($inst) = $target->{'inst'};
	if (defined($inst)) {
		$tname .= " ($inst)";
	}
	my($ttype) = lc($target->{'target-type'});
	return unless validateTType($ct, $ttype, $tname);

	Debug("Retrieving data for target $tname ($ttype)");

    # Determine the list of data sources for this target.
    my(@targetDSs) = split(/,/, $ct->cfg()->{'targettype'}->{$ttype}->{'ds'});
	if (! defined(@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 = $ct->cfg()->{'map'}->{$mapkey};

		if (defined($mapRef)) {
			$match = $mapRef->{'match'};
			if (defined($match)) {
				$match = expandString($match, $target);
			}
			$baseOID = $mapRef->{'base-oid'};
			$oid = mapOid($ct->cfg()->{'oid'}, $baseOID);
		}

		$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;
		}
	}

	# 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.
			$dataSource = $ct->cfg()->{'datasource'}->{lc($ds)}->{'ds-source'};
			$dataSource = expandString($dataSource, $target);
		}

		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");
		$dsIndex++;
	}

	# For each different data source type (snmp, exec, etc.)
	# call the fetcher to retrieve the data.

	my($dsList, $type, @mixedResults);
	while (($type, $dsList) = each %targetDataSources) {
		if (defined ($main::gDSFetch{$type})) {
			push(@mixedResults,
				 &{$main::gDSFetch{$type}}($dsList, $ct, $target));
		} else {
			Warn("Could not find a fetcher with type $type to " .
					"fetch data for $tname.");
		}
	}

	# Reassemble the data in the right order.

	my($line, @results);
	foreach $line (@mixedResults) {
		my($index, $value) = split(/:/, $line, 2);
		$results[$index] = $value;
	}

	# 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;
			}
		}
	}

	# if we are verifying, check the
	# fetched mapping key to make certain it's right

	if (defined($target->{'--verify-mapkey--'})) {
		my($fetchedKey) = pop @results;
		my($wrongInst);

		if ($match =~ /^\s*\/(.*)\/\s*$/) {
			$match = $1;
			$wrongInst = ($fetchedKey !~ /$match/);			
		} 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.

			mapInstance($ct, $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($ct, $target);
			} else {
				# fill in all unknown, since the mapping key seems
				# to no longer exist
				@results = ();
				for ($ctr = 0; $ctr < $dsCount; $ctr++) {
					push @results, "U";
				}
			}

		}
	}

    # Make sure that we have the same number of return values
    # as datasources.
	my($numRes) = $#results+1;
    if ( $numRes != $dsCount ) {
		Warn("$dsCount datasources required, $numRes results returned!");
		@results = ();
    } 

	Info("Retrieved data for $tname: ", join(",", @results));
	Info("Some data is missing for $tname.") if ($missingData);

    return @results;
}

sub newRRD {
	# Create a new RRD file base on the contents of the
	# referenced dictionary.
	my($ct, $target) = @_;

	my($dsRef) = $ct->cfg()->{'datasource'};
	my($rra) = $ct->cfg()->{'rra'};

	my($datafile) = $target->{'rrd-datafile'};
	my($tname) = $target->{'auto-target-name'};

	my($ttype) = lc($target->{'target-type'});
	return unless validateTType($ct, $ttype, $tname);

	# Create the data directory if it does not exist:
	$datafile =~ /(.*)\/.*$/;
	my($dataDir) = $1;
	if (! -d $dataDir) {
		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;

  LOOP:
	while (($type, $val) = each(%{$ct->cfg()->{'targettype'}->{$ttype}})) {
		$valid = 1;
		if ($type eq "ds") {
			my($dss, $ds);
			$dss = $val;
			foreach $ds (split(/,/, $dss)) {
				my($dsname) = lc($ds);
				if (defined($dsRef->{$dsname})) {
					my($d) = $dsRef->{$dsname};

					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));

					push @dsDesc, join(":", 'DS', "ds$dsCnt",
						$dst, $hb, $min, $max);
					$dsCnt++;

				} else {
					Warn("Datasource $ds referenced by target " .
							"type $ttype does not exist.");
					$valid = 0;
					last LOOP;
				}
			}
		} elsif ($type eq "rra" ) {
			my($rras, $theRra);
			$rras = $val;
			foreach $theRra (split(/,/, $rras)) {
				if (defined $rra->{lc($theRra)}) {
					push(@rraDesc, "RRA:$rra->{lc($theRra)}");
				} else {
					Warn("RRA $theRra referenced by target " .
							"type $ttype does not exist.");
					$valid = 0;
					last LOOP;
				}
			}
		} else {
			# this is some other target type attribute (like view).
			# skip it.
		}
	}

	if ($valid) {
		push(@arg, @dsDesc, @rraDesc);

		Info("Creating datafile $datafile for target name $tname.");
		RRDs::create(@arg);
		if (my $error = RRDs::error()) {
			Warn("Cannot create $datafile: $error\n");
			$valid = 0;
		}
	} else {
		Error("Errors prevented the creation of $datafile");
	}

	return $valid;
}

