The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!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__