#!/usr/bin/perl

use NetSNMP::manager::getValues qw(getValues);
use SNMP;
use DBI;
use Net::SMTP;

#===========================================================================
#  Global defines
#===========================================================================

$hostname = 'localhost';          # Host that serves the mSQL Database
$dbname = 'snmp';                 # mySQL Database name
$smtpserver = 'localhost';
$smtpfrom   = 'Net-SNMP Manager <wjhardaker@ucdavis.edu>';	# <===  CHANGE ME  ========
$doit = 1;
$somehosts = 0;

sub usage {
    print "$0 [-H host] [-u user] [-p password] [-l hostlist,...] [-v] [-h] [-n] [-d] [-m mib-to-load] <-m mibnode>\n";
    exit 0;
}

while ($#ARGV > -1) {
    $_ = shift @ARGV;
    usage if (/-h/);
    $hostname = shift if (/-H/);
    if (/-l/) {
	my $arg = shift;
	my @a = split(/,/,$arg);
	my $i;
	$somehosts = 1;
	foreach $i (@a) {
	    $dohost{$i} = 1;
	}
    }
    $user = shift if (/-u/);
    $pass = shift if (/-p/);
    $verbose = 1 if (/-v/);
    $delete = 1 if (/-d/);
    $doit = 0 if (/-n/);
    $tableexpr = shift if (/-t/);
    if (/-m/) {
	# load some mibs
	# SNMP::loadModules(shift);
	$ENV{'MIBS'} = shift;
    }
    if (/-M/) {
	# add a mib directory to look in
	$ENV{'MIBDIRS'} = shift;
	# SNMP::addMibDirs(shift);
    }
}

init_mib;

#===========================================================================
# Connect to the mSQL database with the appropriate driver
( $dbh = DBI->connect("DBI:mysql:database=$dbname;host=$hostname", $user, $pass))
    or die "\tConnect not ok: $DBI::errstr\n";

#
# delete history rows every so often.
#
my %count = getValues($dbh, 'setup', 'deletecount');

if (!defined($count{'max'})) {
    # default is to delete history rows once an hour.
    $dbh->do("insert into setup values('deletecount','max','6')");
    $count{'max'} = 6;
}

if (!defined($count{'current'})) {
    $dbh->do("insert into setup values('deletecount','current','0')");
} else {
    $count{'current'}++;
    if ($count{'max'} <= $count{'current'}) {
	$count{'current'} = 0;
	$deletehist = 1;
    }
    $dbh->do("update setup set valcol = $count{'current'} where lookup = 'deletecount' and varcol = 'current'");
}

#===========================================================================
# Get host records from database and process

$cursor = getcursor("SELECT distinct host FROM hosttables");
nexthost: while (  $hostrow = $cursor->fetchrow_hashref ) {

    my $host = $hostrow->{'host'};

    next if ($somehosts && !defined($dohost{$host}));

    #set up the session
    print STDERR " starting $host\n" if ($verbose);
    my $x = $dbh->prepare("select groupname from hostgroups where host = '$host'");
    my $y = $x->execute();
    my $group = ${$x->fetchrow_hashref}{'groupname'};
    my @args = ('authgroup','default');
    print STDERR "$host...$y\n" if ($verbose);
    if (defined($y) && "$y" ne "0E0") {
        push @args,'authgroup',$group;
    }
    push @args,'authhost',$host;
    print STDERR "$host: $group\n" if ($verbose);

    print STDERR "authvals: ", join(", ", @args), "\n" if ($verbose);
    my %authvals = getValues($dbh, @args);
    if ($verbose) {
	print STDERR "parms for $host:";
	foreach my $i (keys(%authvals)) {
	    print STDERR "$i => $authvals{$i}, ";
	}
	print STDERR "\n";
    }

    my $sess = new SNMP::Session (  DestHost => $host, 
				    UseSprintValue => 1,
				    %authvals );
    print STDERR "Sess ($host): $sess, ref=" . ref($sess). "\n" if ($verbose);
    if (ref ($sess) ne "SNMP::Session") {
#	print STDERR "ack: \$sess not a SNMP::Session for $host ($!)\n";
	hosterror("$host");
	next nexthost;
    }

    # get various bits of system information.
    my $sysDescr = $sess->get('sysDescr.0');
    my $sysId = SNMP::translateObj($sess->get('sysObjectID.0'));
    my $versiontag = $sess->get('versionTag.0');
    my $sysuptime = $sess->get('sysUpTime.0');

    if ($sysDescr eq "" || $sysId eq "" || $versiontag eq "" || 
	$sysuptime eq "") {
	hosterror("$host","Problem collecting basic info");
	next;
    }

    $dbh->do("update hostgroups set sysObjectId = '$sysId', sysDescr = '$sysDescr', versionTag = '$versiontag', sysUpTime = '$sysuptime' where host = '$host'");

    # translate the sysUpTime to a real number for future use:
    {
	my ($d,$h,$m,$s,$fs) = ($sysuptime =~ /^(\d+):(\d+):(\d+):(\d+)\.(\d+)$/);
	$sysuptime = $fs + $s*100 + $m*100*60 + $h*100*60*60 + $d*100*60*60*24;
    }

    # get a list of tables we want to store
    $cmd = "SELECT * FROM hosttables where (host = '$host')";
    print STDERR " $cmd\n" if ($verbose);
    ( $tblh = $dbh->prepare( $cmd ) )
	or warn "\nnot ok: $DBI::errstr\n";
    ( $tblh->execute )
	or print( "\tnot ok: $DBI::errstr\n" );

    while (  $tablelist = $tblh->fetchrow_hashref ) {
	next if (defined($tableexpr) && $tablelist->{'tablename'} !~ /$tableexpr/);
	print STDERR "starting table $tablelist->{'tablename'}\n" if ($verbose);
	my $mib = $SNMP::MIB{SNMP::translateObj($tablelist->{'tablename'})};
	if (!$mib) {
	    warn "mib node $tablelist->{'tablename'} doesn't exist";
	    next;
	}
	my $children = get_children($mib);

	# create the table in our database if it doesn't exist.
	setuptable($dbh, $tablelist->{tablename}, $delete);
	if ($tablelist->{'keephistory'} > 0) {
	    setuptable($dbh, $tablelist->{tablename}, $delete, "hist");
	}
	
	$var = 
	    new SNMP::Varbind([SNMP::translateObj($tablelist->{'tablename'})]);
	my $void = SNMP::translateObj($tablelist->{'tablename'});
	my $val = $sess->getnext($var);
	print STDERR "init err: $sess->{'ErrorStr'}\n" if ($verbose);
	if ($sess->{'ErrorStr'} =~ /Timeout/) {
	    print STDERR "$host timed out\n" if ($verbose);
	    hosterror($host);
	    next nexthost;
	}
	$initlabel = "";
	print STDERR " starting $tablelist->{tablename}\n" if ($verbose);
	my %tbl_ids;
	while (1) {
	    my $varlabel = $var->[$SNMP::Varbind::tag_f];
	    print STDERR "last $host " . SNMP::translateObj($varlabel) . ": $void\n" if ($verbose && SNMP::translateObj($varlabel) !~ /^$void/);

	    last if (SNMP::translateObj($varlabel) !~ /^$void/);
	    $varlabel = SNMP::translateObj($var->[$SNMP::Varbind::tag_f]) if ($varlabel =~ /^[\.0-9]+$/);
	    $initlabel = $varlabel if ($initlabel eq "");

	    my $val = $sess->getnext($var);
	    if ($sess->{'ErrorStr'} =~ /Timeout/) {
		print STDERR "$host timed out\n" if ($verbose);
		hosterror($host);
		next nexthost;
	    }
	    last if ($sess->{'ErrorStr'});
	    my $id = $var->[$SNMP::Varbind::iid_f];
	    print STDERR "$initlabel = $varlabel\n" if ($verbose);
	    last if ($varlabel ne $initlabel);
	    my %vals;
	    $tbl_ids{$id} = 1;
	    foreach $c (@$children) {
		my $oid = $$c{'objectID'} . "." . $id;
		my $newvar = new SNMP::Varbind([$oid]);
		my $val = $sess->get($newvar);
		my $label = SNMP::translateObj($$c{'objectID'});
		$vals{$label} = $val;
	    }
	    my $cmd;

	    # check to see if the error previously existed and then
	    # delete the old entry.
	    my $olderr =
		checkrowforerrors($tablelist->{'tablename'}, $host, $id);
	    $dbh->do("delete from $tablelist->{tablename} where ( host = '$host'  and oidindex = '$id')");
	    $res = $dbh->do("select * from $tablelist->{'tablename'} where ( host = '$host' and oidindex = '$id')");
	    print STDERR "  result: $res\n" if ($verbose);
	    if ($res ne "0E0") {
		$cmd = "update $tablelist->{'tablename'} set ";
		foreach $h (keys(%vals)) {
		    $cmd .= "$h = '$vals{$h}', ";
		}
		$cmd .= " updated = NULL where (host = '$host' and oidindex = '$id')";
		
	    } else {
		$cmd = "insert into $tablelist->{'tablename'}(host, oidindex, " . join(", ",keys(%vals)) .
		    ") values('$host', '$id', '" .
			join("', '",values(%vals)). "')";
	    }

	    print STDERR "  $cmd\n" if ($verbose);
	    $dbh->do("$cmd")
		or warn "\nnot ok: $cmd => $DBI::errstr\n" if ($doit);

	    if ($tablelist->{'keephistory'} > 0) {
		$cmd = "insert into $tablelist->{'tablename'}hist (host, oidindex, sysUpTime, " 
		    . join(", ",keys(%vals))
		    . ") values('$host', '$id', $sysuptime, '"
		    . join("', '",values(%vals)). "')";
		print STDERR "  $cmd\n" if ($verbose);
		$dbh->do("$cmd")
		    or warn "\nnot ok: $cmd -> $DBI::errstr\n" if ($doit);
		
	    }

	    my $newerr = 
		checkrowforerrors($tablelist->{'tablename'}, $host, $id);
	    if ($newerr->{retval} != $olderr->{retval}) {
		 logerror($host, $newerr->{retval}, $newerr->{errfield}, 
			  $newerr->{errvalue});
	     }
	} # snmp loop

	# delete the data beyond the number of days requested.
	if ($deletehist && $tablelist->{'keephistory'} > 0) {
	    $dbh->do("delete from $tablelist->{'tablename'}hist where (unix_timestamp() - unix_timestamp(updated)) > $tablelist->{'keephistory'}*24*60*60 and host = '$host'") or warn "\nnot ok: $DBI::errstr\n" if ($doit);
	}

	my $curs = getcursor("select oidindex from $tablelist->{tablename} where host = '$host'");
	my $row;
	while ($row = $curs->fetchrow_hashref) {
	    print STDERR "  $row->{oidindex}\n" if ($verbose);
	    if (!defined($tbl_ids{$row->{oidindex}})) {
		$dbh->do("delete from $tablelist->{tablename} where oidindex = '$row->{oidindex}'");
		print STDERR "deleting: $host $tablelist->{tablename} $row->{oidindex}\n" if ($verbose);
	    }
	}
	print STDERR "  done with $tablelist->{tablename}\n" if ($verbose);
    } # table loop

    if (isbadhost($host)) {
	# let them out, they're no longer being bad.
	print STDERR "deleting: delete from hosterrors where host = '$host'\n" if ($verbose);
	$dbh->do("delete from hosterrors where host = '$host'");
	mailusers("$host responding again", "$host responding again",
		  getoncallforhost($host));
    }
    print STDERR "  done with $host\n" if ($verbose);
} # host loop

# disconnect
$cursor->finish();
$dbh->disconnect();

#
# Subroutines
#

# setup a table in the database based on a MIB table.
sub setuptable {

    my %conversions = qw(INTEGER integer INTEGER32 integer OCTETSTR varchar(254) COUNTER integer UINTEGER integer IPADDR varchar(254) OBJECTID varchar(254) GAGUE integer OPAQUE varchar(254) TICKS integer GAUGE integer);

    # set up mib info
    my ($dbh, $mibnode, $delete, $suffix) = @_;

    my $mib = $SNMP::MIB{SNMP::translateObj($mibnode)};
    my $children = get_children($mib);
    my ($cmd, $j);

    if ($delete) {
	$cmd = "drop table if exists $mib->{label}";
	print STDERR "cmd: $cmd\n" if ($verbose);
	$dbh->do($cmd)
	    or warn "\nnot ok: $cmd -> $DBI::errstr\n" if ($doit);
    } elsif (($ret = $dbh->do("show tables like '$mib->{label}$suffix'")) ne "0E0") {
	# the table already exists
	return;
    }

    print STDERR "show tables like $mib->{label}$suffix: $ret\n" if($verbose);
    print STDERR " creating table for $mibnode ($mib->{label}$suffix)\n" if ($verbose);
    
    $cmd = "create table $mib->{label}$suffix (id integer auto_increment primary key, host varchar(32) not null, oidindex varchar(32) not null";
    foreach $j (sort { $a->{'subID'} <=> $b->{'subID'} } @$children) {
	if (!defined($conversions{$j->{type}})) {
	    print STDERR "no conversion for $j->{label} = ". $j->{type} . "!\n";
	    return;
	}
	$cmd .= ", $j->{label} $conversions{$j->{type}}";
    }
    $cmd .= ", updated timestamp";
    $cmd .= ", sysUpTime integer" if (defined($suffix));
    $cmd .= ",key oidindex (oidindex), key host (host))";

    print STDERR "cmd: $cmd\n" if ($verbose);
    $dbh->do("$cmd")
	or warn "\nnot ok: $cmd -> $DBI::errstr\n" if ($doit);

}

sub getoncall {
    my @groups = @_;
    my $cur;
    my $row;
    my ($emails, @days, @hours, @two, $i);
    my %dayscon = qw(Sun 0 Mon 1 Tue 2 Wed 3 Thu 4 Fri 5 Sat 6);
    my @now = localtime(time());
    my %people;
    my $group;

    foreach $group (@groups) {
	$cur = getcursor("select * from oncall where groupname = '$group'");
      row: while (  $row = $cur->fetchrow_hashref ) {
	  @days = split(/,/,$row->{'days'});
	  foreach $i (@days) {
	      @two = split(/-/,$i);
	      if ($row->{'days'} eq "*" ||
		  (defined($dayscon{$i}) && $dayscon{$i} == $now[6]) ||
		  (defined($dayscon{$two[0]}) && defined($dayscon{$two[1]}) &&
		   (($dayscon{$two[0]} <= $now[6] && 
		     $dayscon{$two[1]} >= $now[6]) ||
		    (($dayscon{$two[0]} > $dayscon{$two[1]}) &&
		     ($dayscon{$two[0]} <= $now[6] || 
		      $dayscon{$two[1]} >= $now[6]))))) {
		  # we hit a valid day range
		  print STDERR "    hit it $row->{'email'} $now[6]\t($i)\t$row->{'days'}\n"
		      if ($verbose);
		  $people{$row->{'email'}} = $row->{'email'};
	      } else {
		  print STDERR "not hit it $row->{'email'} $now[6]\t($i)\t$row->{'days'}\n"
		      if ($verbose);
	      }	      
	  }
      }
    }
    return keys(%people);
}

sub getoncallforhost {
    my $host = shift;
    return getoncall(getgroupsforhost($host));
}

sub getcursor {
    my $cmd = shift;
    my $cursor;
    print STDERR "cmd: $cmd\n" if ($verbose);
    ( $cursor = $dbh->prepare( $cmd ))
	or die "\nnot ok: $DBI::errstr\n";
    ( $cursor->execute )
	or print( "\tnot ok: $DBI::errstr\n" );
    return $cursor;
}

my %expressions;
sub getexpr {
    my $table = shift;
    print "ref: ",ref($expressions{$table}),"\n" if ($verbose);
    if (!defined($expressions{$table})) {
	my $exprs = getcursor("SELECT * FROM errorexpressions where (tablename = '$table')");
	while (  $expr = $exprs->fetchrow_hashref ) {
	    push @{$expressions{$table}{'expr'}},$expr->{expression};
	    push @{$expressions{$table}{'returnfield'}},$expr->{returnfield};
	}
    }
    if (ref($expressions{$table}) ne "HASH") {
	# no expressions for this table.
	$expressions{$table}{'expr'} = [];
	$expressions{$table}{'returnfield'} = [];
    }
    return $expressions{$table};
}

sub checkrowforerrors {
    my ($table, $host, $id) = @_;
    my $error;

    my $lastres = 0, $lastfield = '';
    my $expressions = getexpr($table);
    my $i;
    for($i=0; $i <= $#{$expressions->{'expr'}}; $i++) {
	if (!defined($expressions->{'prepared'}[$i])) {
	    $expressions->{'prepared'}[$i] = $dbh->prepare("select * from $table where $expressions->{'expr'}[$i] and host = ? and oidindex = ?")
		or warn "\nnot ok: $DBI::errstr\n";
	    print STDERR "preparing select * from $table where $expressions->{'expr'}[$i] and host = ? and oidindex = ? ==> ",ref($expressions->{'prepared'}[$i]),"\n" if($verbose);
	}
	my $prepared = $expressions->{'prepared'}[$i];
	print STDERR "x: ",ref($prepared),"\n" if($verbose);
	$prepared->execute($host, $id) or warn "\nnot ok: $DBI::errstr\n";
	while (  $error = $prepared->fetchrow_hashref ) {
	    print STDERR "$host: $expressions->{returnfield}[$i] = $error->{$expressions->{returnfield}[$i]}\n" if ($verbose);
	    return {'retval', 1,
		    'errfield', $expressions->{returnfield}[$i],
		    'errvalue', $error->{$expressions->{returnfield}[$i]}};
	}
	$lastres = $error->{$expressions->{returnfield}[$i]};
	$lastfield = $expressions->{returnfield}[$i];
    }
    return {'retval', 0, 
	    'errfield', $lastfield,
	    'errvalue', $lastres};
}

sub logerror {
    my ($host, $err, $field, $result) = @_;
    my $groups = getcursor("SELECT distinct groupname FROM hosttables where host = '$host'");
    my ($group, $person);
    my $msg = (($err) ? "error" : "normal");
		    
    my @people = getoncallforhost($host);
    $msg = "$msg: $host";
    $msg .= " $field = $result" if ($field || $result);
    mailusers("SNMP: $msg: $host $field", "$msg\n", @people);
}

sub mailusers {
    my $subject = shift;
    my $msg = shift;
    my @people = @_;
    my $person;
    my $smtpsock = Net::SMTP->new($smtpserver);

    $smtpsock->mail($smtpfrom);
    my $error = $smtpsock->recipient(@people);
    if (!$error) {
	print STDERR "failed to send mail to ",join(",",@people),"\n";
    }
    $smtpsock->data();
    $subject =~ s/\n//;
    $smtpsock->datasend("To: " . join(", ",@people) . "\n");
    $smtpsock->datasend("From: $smtpfrom\n");
    $smtpsock->datasend("Subject: $subject\n");
    $smtpsock->datasend("\n");
    $smtpsock->datasend("$msg\n");
    $smtpsock->dataend();
    $smtpsock->quit;
    print STDERR "mailed ",join(",",@people)," with $msg, $subject ($!)\n" if ($verbose);
}

sub hosterror {
    my $host = shift;
    my $error = shift || "No response";
    my $groups = getcursor("SELECT distinct groupname FROM hosttables where host = '$host'");
    my ($group, $person);
    my %mailed;

    return if (isbadhost($host)); # only send out a message once.
		    
    $dbh->do("insert into hosterrors(host, errormsg) values('$host','$error');");
    my @people = getoncallforhost($host);
    mailusers("No Response from $host", "$host: $error", @people);
}

sub isbadhost {
    my $host = shift;
    my $hosterr = getcursor("SELECT distinct host FROM hosterrors where host = '$host'");
    if ($hosterr->fetchrow_hashref) {
	return 1;
    }
    return 0;
}

sub getgroupsforhost {
    my $host = shift;
    my @retgroups;
    my $groups = getcursor("SELECT distinct groupname FROM hosttables where host = '$host'");
    while( $group = $groups->fetchrow_hashref ) {
	push @retgroups,$group->{'groupname'};
    }
    @retgroups;
}

sub get_children {
    my $mib = shift;
    my $children = $$mib{'children'};
    if (ref($children) ne "ARRAY") {
	warn "$mib has no chlidren";
	return;
    }

    if ($#{$children} == 0 && $mib->{'label'} =~ /Table$/) {
	# is a table, use entry?
	$children = $children->[0]{'children'};
	if (ref($children) ne "ARRAY") {
	    warn "$mib has no chlidren";
	    return;
	}
    }
    return $children;
}
