#!/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',
	ifName => '1.3.6.1.2.1.31.1.1.1.1',
	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 = "node_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, ignore_ports 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])
			{
			$device[3] = $row[4];
			$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 COUNT(*) FROM nodes WHERE mac_addr = ? ;");
	my $update_arp = $dbconn->prepare("UPDATE nodes SET last_updated = CURRENT_TIMESTAMP, ip_addr = ? WHERE mac_addr = ?;");
	my $insert_arp = $dbconn->prepare("INSERT INTO nodes (mac_addr, ip_addr, last_updated) VALUES (?,?,CURRENT_TIMESTAMP);");
	my $update_cam = $dbconn->prepare("UPDATE nodes SET last_updated = CURRENT_TIMESTAMP, switch = ?, vlan = ?, " .
		"module = ?, port = ? WHERE mac_addr = ?;");
	my $insert_cam = $dbconn->prepare("INSERT INTO nodes (mac_addr, switch, vlan, module, port, last_updated) 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 ($switch, $mac_addr, $module, $port, $vlan) = @request;
			#find any record matching the mac_addr
			unless ( $select->execute( $mac_addr) )
				{ safe_print("$name:Database select failed:" .  $dbconn->errstr, "\n"); next }

			#if we got an existing record, UPDATE it, otherwise INSERT a new one
			if ($select->fetchrow_array)
				{
				unless ( $update_cam->execute($switch, $vlan, $module, $port, $mac_addr) )
					{ safe_print("$name:Database update failed:" .  $dbconn->errstr, "\n"); next }
				}
			else
				{
				unless ( $insert_cam->execute($mac_addr, $switch, $vlan, $module, $port ) )
					{ safe_print("$name:Database insert failed:" .  $dbconn->errstr, "\n"); next }
				}
			$dbconn->commit or safe_print("$name:COMMIT database transaction failed:" .  $dbconn->errstr);
			}
		elsif ($type == 2)
			{
			my ($mac_addr, $ip_addr) = @request;
			
			#find any record matching the mac_addr
			unless ( $select->execute( $mac_addr) )
				{ safe_print("$name:Database select failed:" .  $dbconn->errstr, "\n"); next }

			#if we got an existing record, UPDATE it, otherwise INSERT a new one
			if ($select->fetchrow_array)
				{
				unless ( $update_arp->execute($ip_addr, $mac_addr) )
					{ safe_print("$name:Database update failed:" .  $dbconn->errstr, "\n"); next }
				}
			else
				{
				unless ( $insert_arp->execute($mac_addr, $ip_addr) )
					{ safe_print("$name:Database insert failed:" .  $dbconn->errstr, "\n"); next }
				}
			$dbconn->commit or safe_print("$name:COMMIT database transaction failed:" .  $dbconn->errstr, "\n");
			}
		elsif ($type == 3)
			{
			print @request
			}
		}
	$select->finish;
	$update_arp->finish;
	$insert_arp->finish;
	$update_cam->finish;
	$insert_cam->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, $ignore_ports) = @_;
	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, $ignore_ports )
			}
		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, $ignore_ports ) }
	
	}
		
sub collect
	{
	my ($target,$target_name,$community, $ignore_ports) = @_;
	
	my ($snmp) = Net::SNMP->session( -hostname => $target,
		-community => $community, -version => '2c');
	
	if (has_mibs($snmp, $oids{ciscoVtp}, $oids{ifMIB}, $oids{dot1dBridge} ))
		{
		my ($key, %ignore_ports, $ifIndex, $ifName);
		#get the list of trunk ports
		my ($vtpTrunk) = $snmp->get_table($oids{vtpTrunkStatus});
		foreach $oid (keys(%$vtpTrunk))
			{
			#DynamicStatus 1 = on
			unless ( $vtpTrunk->{$oid} == 1 ) { next }
			my ($ifIndex) = ($oid =~ /\.(\d+)$/);
			$ignore_ports{$ifIndex} = 1;
			}
		foreach $ifIndex (split(':', $ignore_ports))
			{
			$ignore_ports{$ifIndex} = 1;
			}
		unless ($ifName = $snmp->get_table($oids{ifName}) )
			{
			safe_print('failed to get ifName ' . $target . ' ' . $snmp->error . "\n");
			next
			}
		#collect from all instances on the bridge MIB, one per VLAN.
		my $vlanNames = $snmp->get_table($oids{vtpVlanName});
		foreach $key (keys(%$vlanNames))
			{
			($key =~ /.(\d+)$/);
			my $vlan = $1; $vlans .= $vlan . ' ';
			if ($vlan > 1001 && $vlan < 1006 ) { next }
			my $snmp2 = Net::SNMP->session( -hostname => $target,
				-community => $community . '@' . $vlan, -version => '2c');			
			my ($fdbAddr) = $snmp2->get_table($oids{dot1dTpFdbAddress})
				or next;
			my ($fdbPort) = $snmp2->get_table($oids{dot1dTpFdbPort})
				or next;
			my ($portIfIndex) = $snmp2->get_table($oids{dot1dBasePortIfIndex})
				or next;
			foreach $oid (keys(%$fdbAddr))
				{
				#Sometimes mac addresses come back with nulls in them
				#this screws stuff up, so skip them.
				if ( $fdbAddr->{$oid} =~ /\0/ ) { next }
				my ($addrIndex) = $oid =~ /^\Q$oids{dot1dTpFdbAddress}.\E([\d+\.]+)$/
					or next;
				my $fdbPort = $fdbPort->{$oids{dot1dTpFdbPort} . '.' . $addrIndex}
					or next;
				my $ifIndex = $portIfIndex->{$oids{dot1dBasePortIfIndex} . '.' . $fdbPort}
					or next;
				if ($ignore_ports{$ifIndex})
					{ next }
				my $ifName = $ifName->{$oids{ifName} . '.' . $ifIndex}
					or next;
				my ($module,$port) = ($ifName =~ /(\d+)\/(\d+)$/);
				my $request = [ ];
				$collation_q->enqueue( join( "\0", 1, $target_name, transmac($fdbAddr->{$oid}), $module, $port, $vlan) );
				}
			}
		}
		
	
	if (has_mibs($snmp, $oids{IParp} ) )
		{
		my ($oid);
		my ($arp) = $snmp->get_table($oids{IParp})
			or safe_print($target . ' ' . $snmp->error . "\n"); 
		foreach $oid (keys(%$arp))
			{
			my ($ip_addr) = ($oid =~ /(\d+\.\d+\.\d+\.\d+)$/);
			$collation_q->enqueue( join( "\0", 2, transmac($arp->{$oid}), $ip_addr ) );
			}
		}
	
	}

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 transmac
	{
	my $hexmac = shift;
	my $i = 2;
	my $retval = '';
	while ($i < 12)
		{
		$retval .= substr($hexmac, $i, 2) . '-';
		$i += 2;
		}
	$retval .= substr($hexmac, 12, 2);
	return $retval;
	}

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, @_ ) );
	}
