#!perl
use strict;
use warnings;
use Data::Dumper;
use Carp;
use Parse::CSV;
use SNMP;
use SNMP::Query::Asynch;
#---------------------------------------------------------
my $csv_file = shift || die "Please specify a CSV file with SNMP host info!";
# The required columns in the loaded CSV file.
my @reqired_csv_cols = qw(HOSTIP COMMUNITY SNMPVER SNMPPORT);
my $max_inflight = shift || 50;
my $num_cycles = shift || 1;
my $master_timeout = 0; # Set to number of seconds before
# all queries are terminated.
# 0 means no master timeout.
my $batch_size = 10; # Run a callback whenever this many
# results have been returned
my @varbinds = qw(
ifDescr ifInOctets ifOutOctets ifAlias ifType
ifName ifInErrors ifOutErrors ifSpeed
ifAdminStatus ifOperStatus
);
#---------------------------------------------------------
# This probably isn't necessary, but it's the Right Thing To Do
# so the SNMP module won't be forced to do this internally instead.
# (the process is a lot more involved and careful in there, thus slower)
my $varlist = SNMP::VarList->new( map { [$_] } @varbinds );
# Load the CSV file then clean out any invalid data.
my @hosts = read_hosts_csv($csv_file, @reqired_csv_cols);
@hosts = clean_hosts_data(\@hosts);
# This object encapsulates the desired queries to run.
my $query = SNMP::Query::AsynchMulti->new();
# We're going to install this callback to run before every query.
my $preop_callback = sub {
warn "+ IF/TI/GI: " . $query->current_in_flight()
. "/" . $query->this_run_issued()
. "/" . $query->grand_total_issued()
. "\n"
;
};
# We're going to install this callback to run after every query.
# Yes, I know I'm duplicating code. Would you rather I obfuscate it?
my $postop_callback = sub {
warn "- IF/TF/GF: " . $query->current_in_flight()
. "/" . $query->this_run_finished()
. "/" . $query->grand_total_finished()
. "\n"
;
};
# Add a query operation for each host to the $query object.
foreach my $host (@hosts)
{
$query->add_getbulk({
# Params concerning the SNMP Session
DestHost => $host->{HOSTIP},
Community => $host->{COMMUNITY},
Version => $host->{SNMPVER},
RemotePort => $host->{SNMPPORT},
#Timeout => $host->{SNMP_TIMEOUT},
#Retries => $host->{SNMP_RETRIES},
# Params concerning the type of query operation
MaxRepeaters => 20,
NonRepeaters => 0,
# The varbinds to be operated on
VarBinds => $varlist,
# Callbacks before and after this query op.
PreCallback => $preop_callback, # Do this before the query
PostCallback => $postop_callback, # Do this after the query
});
warn "Added query to: $host->{HOSTIP}\n";
}
# This will be registered as a callback that is called after a 'batch'
# of queries has completed.
my $batch_callback = sub {
my $results_ref = $query->get_results_ref();
my @results;
push @results, pop @$results_ref
while scalar @$results_ref;
print "BATCH RESULTS\n" . Dumper \@results;
};
# Run all the added queries with up to $max_inflight
# asynchronous operations in-flight at any time.
# Lather, rinse, repeat for $num_cycles.
warn "Beginning polling cycle\n";
foreach my $iter ( 1..$num_cycles )
{
sleep 30 unless $iter == 1;
# Randomize order of queries...(not yet implemented)
# I want this feature because I will be repeatedly polling these same
# devices. Using the same order every time can actually cause 'phantom'
# capacity issues, usually caused *by* the polling. Randomizing helps
# smooth out any potiential impact the polling order may otherwise have.
warn "Shuffling queries (not yet implemented)\n";
$query->shuffle();
# Execute the queries that were added. See the POD for more info
# on the parameters given here.
my $results = $query->execute({
InFlight => $max_inflight,
MasterTimeout => $master_timeout,
BatchSize => $batch_size,
BatchCallback => $batch_callback,
});
# In this case, the $batch_callback should have taken care
# of all the results. Therefore, this is a sanity check to
# make sure it worked properly.
print Dumper $results;
}
# TODO I probably need some error-indicator methods for AsynchMulti.
# Something that pushes error status messages onto a stack for later use.
exit;
#---------------------------------------------------------
# Read in the CSV file.
sub read_hosts_csv {
my $file = shift;
my @required_fields = @_;
# Parse entries from a CSV file into hashes hash
my $csv_parser = Parse::CSV->new(
file => $file,
fields => 'auto', # Use the first line as column headers,
# which become the hash keys.
);
my @node_cfg; # Return a reference to this
my $line_num = 0;
while ( my $line = $csv_parser->fetch() ) {
$line_num++;
my $error_flag = 0;
foreach my $field (@required_fields) {
if ( ! exists $line->{$field} ) {
$error_flag = 1;
carp "Missing field [$field] on line [$line_num] in CSV file [$file]";
}
}
croak "Terminating due to errors on line [$line_num] in CSV file [$file]"
if $error_flag;
push @node_cfg, $line;
}
if ( $csv_parser->errstr() ) {
croak "Fatal error parsing [$file]: " . $csv_parser->errstr();
}
return @node_cfg;
}
sub clean_hosts_data {
my $hosts_data = shift;
my @clean_hosts;
foreach my $host (@$hosts_data) {
# Maybe put in a loop to scrub leading and trailing
# whitespace from each field? Yeah, I know. map in
# void context is the devil's work, yadda, yadda.
map { s/^\s*|\s*$//g } values %$host;
if (
$host->{SNMPVER} == 2 #=~ /^1|2c?|3$/
&& $host->{SNMPPORT} =~ /^\d+$/
&& $host->{HOSTIP} =~ /^(?:\d{1,3}\.){3}\d{1,3}$/ # Flawed, but Good Enough.
&& $host->{COMMUNITY}
)
{
push @clean_hosts, $host;
}
else
{
warn "Invalid host data - skipping:\n"
. " " . Dumper($host) . "\n";
}
}
return @clean_hosts;
}
1;
__END__