#!/home/tracker/perl/bin/perl

use lib '/home/tracker';
use threads;
use threads::shared;
use Thread::Semaphore;
use Thread::Queue;

%oids = (
	sysName => '1.3.6.1.2.1.1.5.0',
	IParp => '1.3.6.1.2.1.4.22.1.2',
	dot1dBridge => '1.3.6.1.2.1.17',
	dot1dTpFdbAddress => '1.3.6.1.2.1.17.4.3.1.1',
	dot1dTpFdbPort => '1.3.6.1.2.1.17.4.3.1.2',
	dot1dBasePortIfIndex => '1.3.6.1.2.1.17.1.4.1.2',
	ifMIB => '1.3.6.1.2.1.31',
	interfaces => '1.3.6.1.2.1.2',
	ifName => '1.3.6.1.2.1.31.1.1.1.1',
	ifDescr => '1.3.6.1.2.1.2.2.1.2',
	ifAdminStatus => '1.3.6.1.2.1.2.2.1.7',
	ifInOctets => '1.3.6.1.2.1.2.2.1.10',
	ifHighSpeed => '1.3.6.1.2.1.31.1.1.1.15',
	ciscoVtp => '1.3.6.1.4.1.9.9.46',
	vtpVlanName => '1.3.6.1.4.1.9.9.46.1.3.1.1.4',
	vtpTrunkStatus => '1.3.6.1.4.1.9.9.46.1.6.1.1.14',
	vlanMembership => '1.3.6.1.4.1.9.9.68',
	vmVlan => '1.3.6.1.4.1.9.9.68.1.2.2.1.2',
	entLogicalTable => '1.3.6.1.2.1.47.1.2.1',
	entLogicalEntry => '1.3.6.1.2.1.47.1.2.1.1',
	entLogicalIndex => '1.3.6.1.2.1.47.1.2.1.1.1',
	entLogicalDescr => '1.3.6.1.2.1.47.1.2.1.1.2',
	entLogicalType => '1.3.6.1.2.1.47.1.2.1.1.3',
	entLogicalCommunity => '1.3.6.1.2.1.47.1.2.1.1.4',
	);
#queue for incoming work
my $work_q = new Thread::Queue;
#queue of collected data to be added to the DB
my $collation_q = new Thread::Queue;

my $name = "swport_collector";

#Doing all the startup tasks in a seperate thread protects the main thread from using
#DBI or SNMP libraries.
#the postgres DBD does not work correctly across threads
my $startup_thread = threads->new( \&startup );
#hack to get a hash back from the startup thread
eval('%config = ' . $startup_thread->join);

my $num_threads = $config{$name . '_threads'};
if ($num_threads > $work_q->pending) { $num_threads = $work_q->pending }

for (1..$num_threads)
	{ threads->new( sub {
		use Net::SNMP;
		while ( $target = $work_q->dequeue_nb )
			{ begin_collection(split("\0", $target)) }
		})
	}

my $collator = threads->new(\&collate);

foreach $thr (threads->list)
	{
	#main thread just sits here until all other threads have exited
	if ($thr->tid && !threads::equal($thr, threads->self) && !threads::equal($thr, $collator))
		{ $thr->join }
	}
#once all the collector threads have exited, tell the collator to exit as well
$collation_q->enqueue(undef);
$collator->join;

sub startup
	{
	use Net::SNMP;
	use DBI;
	use tracker;
	
	my (@row, %config, $dbconn, $sth);
	#connect to the database
	unless ($dbconn = tracker::conndb() )
		{ die "Unable to connect to Database" }

	#Get the configuration variables from the database
	$sth = $dbconn->prepare("SELECT name, value FROM config ORDER BY name;");
	$sth->execute or die "Database select failed:" .  $dbconn->errstr;
	while (@row = $sth->fetchrow_array)
	{
		$config{$row[0]} = $row[1];
		if ( $row[0] =~ /^snmp_string/ )
			{ push @snmp_strings, $row[1] }
	}

	$sth = $dbconn->prepare("SELECT name, address, cached_snmp_string, " .
		"snmp_ro_string FROM devices WHERE status=1 OR status=4;");
	$sth->execute  or die "Database select failed:" .  $dbconn->errstr;
	
	my $update_contacted = $dbconn->prepare("UPDATE devices SET status=1, last_contacted = CURRENT_TIMESTAMP WHERE name = ?;");
	my $update_error = $dbconn->prepare("UPDATE devices SET status = 4, error = " .
		$dbconn->quote("Failed to contact using any SNMP community string") . " WHERE name = ?;");
	my ($device, @devices);
	while (@row = $sth->fetchrow_array)
		{
		my(@device, @strings, $snmp_string);
		$device[0] = $row[0];
		$device[1] = $row[1];
		#contruct a list of snmp_strings, in the order we'll try them
		#first try the cached snmp_string, if there is one
		if ($row[2]) { push @strings, $row[2] }
		#if there is a hard set string that is not the same as the cached one, add it
		if ( $row[3] && ($row[2] ne $row[3]) )
			{ push @strings, $row[3] }
		#add any global strings that aren't already on the list
		foreach $snmp_string (@snmp_strings)
			{
			if (!on_list(\@strings, $snmp_string) )
				{ push @strings, $snmp_string }
			}
		#try each string in turn
		foreach $snmp_string (@strings)
			{
			if (can_snmp($row[1], $snmp_string))
				{
				$device[2] = $snmp_string;
				last
				}
			}
		if ($device[2])
			{
			$update_contacted->execute($device[0]) or safe_print("$name:Database update failed:" .  $dbconn->errstr, "\n");
			$dbconn->commit or safe_print("$name:COMMIT database transaction failed:" .  $dbconn->errstr);
			$work_q->enqueue(join("\0", @device))
			}
		else
			{
			$update_error->execute($device[0]) or safe_print("$name:Database update failed:" .  $dbconn->errstr, "\n");
			$dbconn->commit or safe_print("$name:COMMIT database transaction failed:" .  $dbconn->errstr);
			}
		}
	$update_contacted->finish;
	$update_error->finish;
	$sth->finish;
	$dbconn->disconnect();
	return hashdump(\%config);
	}

#collator handles the database connection
sub collate
	{
	use DBI;
	use tracker;
	my ($dbconn);
	unless ($dbconn = tracker::conndb() )
		{ die "Unable to connect to Database" }
	#prepare the database statements we will later bind values to.
	my $select = $dbconn->prepare('SELECT description, inoctets, adminstatus, adminspeed, vlan ' . 
		'FROM switch_ports WHERE switch = ? AND module = ? AND port = ?;');
	my $update1 = $dbconn->prepare('UPDATE switch_ports SET description = ?, inOctets = ?, adminStatus = ?, ' .
		'adminSpeed = ?, vlan = ?, last_input_increment = CURRENT_TIMESTAMP ' . 
		'WHERE switch = ? AND module = ? AND port = ?;');
	my $update2 = $dbconn->prepare('UPDATE switch_ports SET description = ?, inOctets = ?, adminStatus = ?, ' .
		'adminSpeed = ?, vlan = ? WHERE switch = ? AND module = ? AND port = ?;');
	my $insert = $dbconn->prepare('INSERT INTO switch_ports (switch, module, port, description, ' .
		'inoctets, adminstatus, adminspeed, vlan, last_input_increment) ' .
		'VALUES (?,?,?,?,?,?,?,?, CURRENT_TIMESTAMP);');
	#loop until the main thread tells us to exit by enqueuing an undef
	while ($request = $collation_q->dequeue)
		{
		#only flat scalar values can be placed on a queue
		my @request = split("\0", $request);
		my $type = shift(@request);
		if ($type == 1)
			{
			my ($entry);
			my ($switch, $module, $port, $descr, $inOctets, 
					$adminStatus, $speed, $vlan) = @request;
			
			unless ($select->execute($switch, $module, $port))
				{ safe_print("$name:Database select failed:" .  $dbconn->errstr); next }
		
			#if we got an existing record, UPDATE it, otherwise INSERT a new one
			if ($entry = $select->fetchrow_hashref)
				{
				if (	$entry->{description} ne $descr ||
					$entry->{inoctets} ne $inOctets ||
					$entry->{adminstatus} ne $adminstatus ||
					$entry->{adminspeed} ne $speed ||
					$entry->{vlan} ne $descr )
					{
					if ( $entry->{inoctets} ne $inOctets )
						{
						$update1->execute($descr, $inOctets+0, $adminStatus+0, $speed+0, $vlan+0, $switch, $module, $port)
							or print("$name:Database update failed:" .  $dbconn->errstr . "\n" . join(' ', @request) . "\n");
						}
					else
						{
						$update2->execute($descr, $inOctets, $adminStatus, $speed, $vlan, $switch, $module, $port)
							or safe_print("$name:Database update failed:" .  $dbconn->errstr . "\n");
						}
					}
				}
			else
				{
				$insert->execute($switch, $module, $port, $descr, $inOctets, $adminStatus, $speed, $vlan)
					or safe_print("$name:Database insert failed:" .  $dbconn->errstr, "\n");
				}
			$dbconn->commit or safe_print("$name:COMMIT database transaction failed:" .  $dbconn->errstr . "\n");			
			}
		if ($type == 3)
			{
			print @request
			}
		}
	$select->finish; $update1->finish; $update2->finish; $insert->finish;
	$dbconn->disconnect;
	}


sub begin_collection
#First we must determine if the target has multiple logical entities that must
#be managed separately (clusters)
	{
	my ($name, $target, $community ) = @_;
	my ($entityName);
	my $target_is_cluster = 0;
	my ($oid, $snmp, $err, $entDescr);
	
	#get the description portion of the logical entity table
	$snmp = Net::SNMP->session( -hostname => $target, -community => $community,
		version => '2c');
	$entDescr = $snmp->get_table($oids{'entLogicalDescr'});
	foreach $oid (keys(%$entDescr))
		{
		#skip the logical entity if it is not a cluster member
		unless ( $entDescr->{$oid} eq 'Catalyst 29xx/35xx' ) { next }
		my ($target_name);
		my ($entityIndex) = ($oid =~ /(\d+)$/);
		my ($href) = $snmp->get_request($oids{entLogicalCommunity} . '.' . $entityIndex);
		my ($entityCommunity) = $href->{$oids{entLogicalCommunity} . '.' . $entityIndex};
		if ( ($target_name) =  &quick_get($oids{sysName}, $target, $entityCommunity) )
			{
			$target_is_cluster = 1;
			collect($target, $target_name, $entityCommunity )
			}
		else
			{ safe_print("$name:Could not contact a cluster member belonging to $target, ignoring.\n") }
		}
		
	#there were no cluster members, just collect from the target itself
	unless ($target_is_cluster)
		{ collect( $target, $name, $community ) }
	
	}
		
sub collect
	{
	my ($target,$target_name,$community ) = @_;
	
	my ($snmp) = Net::SNMP->session( -hostname => $target,
		-community => $community, -version => '2c');
	
	if (has_mibs($snmp, $oids{ifMIB}, $oids{vlanMembership}, $oids{interfaces}))
		{
		my ($ifName);
		if ($ifName = $snmp->get_table($oids{ifName}))
			{
			my $ifDescr = $snmp->get_table($oids{ifDescr});
			my $ifAdminStatus = $snmp->get_table($oids{ifAdminStatus});
			my $ifInOctets = $snmp->get_table($oids{ifInOctets});
			my $ifHighSpeed = $snmp->get_table($oids{ifHighSpeed});
			my $vmVlan = $snmp->get_table($oids{vmVlan});
			foreach $oid (keys(%$ifName))
				{
				my ($module,$port) = ($ifName->{$oid} =~ /(\d+)\/(\d+)$/)
					or next;
				my ($ifIndex) = ($oid =~ /\.(\d+)$/);
				my $descr = $ifDescr->{$oids{ifDescr} . '.' . $ifIndex};
				my $adminStatus = $ifAdminStatus->{$oids{ifAdminStatus} . '.' . $ifIndex};
				my $inOctets = $ifInOctets->{$oids{ifInOctets} . '.' . $ifIndex};
				my $speed = $ifHighSpeed->{$oids{ifHighSpeed} . '.' . $ifIndex};
				my $vlan = $vmVlan->{$oids{vmVlan} . '.' . $ifIndex};
				$collation_q->enqueue( join( "\0", 1, $target_name, $module, $port, $descr, $inOctets, 
					$adminStatus, $speed, $vlan) );
				}
			}
		}
	}

sub quick_get
	{
	my ($oid, $host, $community) = @_;
	my ($snmp, $err) = Net::SNMP->session( -hostname => $host, -community => $community );
	unless ($snmp)
		{ safe_print("SNMP failure for $community\@$host $oid: $err\n"); return 0 }
	my ($href) = $snmp->get_request($oid);
	$snmp->close;
	return $href->{$oid}
	}

sub can_snmp
	{
	my ($target, $community) = @_;
	if (quick_get($oids{sysName}, $target, $community)) { return 1 } else { return 0 }
	}
	
sub has_mibs
	{
	my $snmp = shift;
	my @base_oids = @_;
	foreach $base_oid (@base_oids)
		{
		my ($href) = $snmp->get_next_request($base_oid);
		my ($oid, $value) = %$href;
		unless (Net::SNMP::oid_context_match($base_oid, $oid)) { return 0 }
		}
	return 1
	}

sub on_list
	{
	my ($list_ref, $value) = @_;
	my $val;
	foreach $val (@$list_ref)
		{
		if ($val eq $value) { return 1 }
		}
	return 0
	}
	
sub hashdump
	{
	my $href = shift;
	my $key;
	my @return;
	foreach $key (keys(%$href))
		{ push @return, "$key => '$href->{$key}'" }
	return '(' . join(",\n", @return) . ")\n";
	}

sub safe_print
	{
	$collation_q->enqueue( join( "\0", 3, @_ ) );
	}
