#!/usr/bin/perl
#
# Check aruba wireless 
# 
# $Header: /opt/home/doke/work/nagios/RCS/check_aruba,v 1.25 2009/09/08 20:48:57 doke Exp $


#use strict;   
use warnings;   
no warnings 'redefine';
use Getopt::Long;
use Net::SNMP;
use Storable;
use Fcntl ':flock';
use Time::HiRes qw( usleep );
#use Data::Dumper;


use vars qw( $host $community $aptarget $force $verbose $help $use_snmpv2c
    $mib2 $enterprises $aruba @crits @warns @unknowns @oks $rc $sep
    %snmp_sessions %name2ip $cache_file $ap_cache $cache_holddown_time
    @perfdata );

$host = '';
$community = 'fersnmp';
$aptarget = undef;
$force = 0;
$verbose = 0;
$help = 0;

$use_snmpv2c = 1;

$mib2 = '1.3.6.1.2.1';
$enterprises = '1.3.6.1.4.1';
$aruba = "$enterprises.14823";

$cache_file = "/var/tmp/check_aruba_cache.$<"; 

# don't reload/rebuild the cache more often than this
$cache_holddown_time = 300;   # seconds

sub usage {
    my( $rc ) = @_;
    print "Usage: $0 [-vh] -H <host> [-C <community>] [-a <ap-name>]
    With an ap-name checks on that ap.  
    Without an ap-name checks the controller. 
    -H s  controller hostname [aruba-master]
    -C s  snmp community [public]
    -a s  access point name
    -f    force cache file rebuild, despite age
    -v    verbose
    -h    help
";
    exit $rc;
    }

Getopt::Long::Configure ("bundling");
GetOptions(
    'H=s' => \$host,
    'C=s' => \$community,
    'a=s' => \$aptarget,
    'f' => \$force,
    'v+' => \$verbose,
    'h' => \$help,
    );
&usage( 0 ) if ( $help );
&usage( 0 ) if ( ! $host );
&usage( 0 ) if ( ! $community );

if ( $aptarget ) { 
    check_ap( $aptarget );
    }
else { 
    check_controller();
    }

$rc = 0;
$sep = '';
if ( $#crits >= 0 ) {
    $rc = 2;
    print "CRITICAL ", join( ", ", @crits );
    $sep = '; ';
    }
if ( $#warns >= 0 ) {
    $rc = 1 if ( $rc == 0 );
    print $sep, "Warning ", join( ", ", @warns );
    $sep = '; ';
    }
if ( $#unknowns >= 0 ) {
    $rc = -1 if ( $rc == 0 );
    print $sep, "Unknown ", join( ", ", @unknowns );
    }
if ( $rc == 0 ) {
    print "Ok ", join( ", ", @oks );
    print( " | ", join( ", ", @perfdata ) ) if ( $#perfdata >= 0 );
    }
print "\n";
exit $rc;


##################


sub check_controller {
    my( $result, @oids, @data, $row, $apname );

    my $sysDescr_oid = "$mib2.1.1.0";
    my $sysUpTime_oid = "$mib2.1.3.0";
    my $sysName_oid = "$mib2.1.5.0";
    @oids = ( $sysDescr_oid, $sysUpTime_oid, $sysName_oid );
    $result = snmp_get( $host, $community, 'system', \@oids );
    my $sysDescr = $result->{ $sysDescr_oid };
    my $sysUpTime = $result->{ $sysUpTime_oid };
    my $sysName = $result->{ $sysName_oid };
    if ( $verbose ) { 
	print "sysName $sysName\n";
	print "sysDescr $sysDescr\n";
	print "sysUpTime $sysUpTime\n";
	}
    if ( ! $sysDescr ) { 
	push @unknowns, "unable to get sysDescr";
	return;
	}
    elsif ( $sysDescr !~ m/ArubaOS/ ) { 
	push @unknowns, "not an Aruba controller: $sysDescr";
	return;
	}
    push @oks, "$sysName";
    push @oks, "up $sysUpTime";


    my $role_oid = "$aruba.2.2.1.1.1.4.0";
    my $serialnum_oid = "$aruba.2.2.1.1.1.12.0";
    my $num_aps_oid = "$aruba.2.2.1.1.3.1.0";
    my $num_assocs_oid = "$aruba.2.2.1.1.3.2.0";
    my $swip_oid = "$aruba.2.2.1.2.1.1.0";   # needed to index sysext tables
    my $temp_oid = "$aruba.2.2.1.2.1.10.0";
    #my $num_users_oid = "$aruba.2.2.1.4.1.1.0";   # not useful, wierd counter

    @oids = ( $role_oid, $serialnum_oid, $num_aps_oid, $num_assocs_oid, 
	$swip_oid, $temp_oid );
    $result = snmp_get( $host, $community, 'summary data', \@oids );
    my $role = $result->{ $role_oid };
    my $serialnum = $result->{ $serialnum_oid };
    my $num_aps = $result->{ $num_aps_oid };
    my $num_assocs = $result->{ $num_assocs_oid };
    my $swip = $result->{ $swip_oid };
    my $temp = $result->{ $temp_oid };

    my @roles = ( 'unknown', 'master', 'local', 'backup-master' );
    $verbose && print "role $role $roles[ $role ]\n";
    push @oks, "role " . $roles[ $role ];

    if ( $verbose ) { 
	print "serialnum $serialnum\n";
	print "aps $num_aps\n";
	print "associations $num_assocs\n";
	}
    push @oks, "serialnum $serialnum";
    push @oks, "aps $num_aps";
    push @oks, "associations $num_assocs";

    if ( $temp =~ m/normal/i ) { 
	push @oks, $temp;
	}
    else { 
	push @warns, $temp;
	}

    my $swver_oid = "$aruba.2.2.1.2.1.19.1.4.$swip";
    my $status_oid = "$aruba.2.2.1.2.1.19.1.5.$swip";
    @oids = ( $swver_oid, $status_oid );
    $result = snmp_get( $host, $community, 'version', \@oids );
    my $swver = $result->{ $swver_oid };
    my $status = $result->{ $status_oid };

    push @oks, "version $swver";

    my @statuses = ( 'unknown', 'active', 'inactive' );
    $verbose && print "status $status $statuses[ $status ]\n";
    if ( $status != 1 ) { 
	push @warns, "status " . $statuses[ $status ];
	}
    else { 
	#push @oks, "status " . $statuses[ $status ];
	}


    # processor load table
    @data = walk_table( $host, $community, 'cpuload', "$aruba.2.2.1.2.1.13.1" );
    foreach $row ( 1 .. $#{@{$data[2]}} ) {
	my $descr = $data[2][$row];
	next if ( ! defined $descr );
	my $load = $data[3][$row];
	$verbose && print "$descr load $load%\n";
	if ( $load > 90 ) { 
	    push @crits, "$descr load $load%";
	    }
	elsif ( $load > 80 ) { 
	    push @warns, "$descr load $load%";
	    }
	else  { 
	    #push @oks, "$descr load $load%";
	    }
	}


    # storage table
    @data = walk_table( $host, $community, 'storage', "$aruba.2.2.1.2.1.14.1" );
    foreach $row ( 1 .. $#{@{$data[2]}} ) {
	my $type = $data[2][$row];
	my $size = $data[3][$row];
	my $used = $data[4][$row];
	my $name = $data[5][$row];
	my $pc = int( $used * 100 / $size );
	$verbose && print "$name $pc% full\n";
	if ( $name eq '/' ) { 
	    # skip it, it's always 98% full
	    }
	elsif ( $pc > 90 ) { 
	    push @crits, "$name $pc% full";
	    }
	elsif ( $pc > 80 ) { 
	    push @warns, "$name $pc% full";
	    }
	else  { 
	    #push @oks, "$name $pc% full";
	    }
	}


    # memory table
    @data = walk_table( $host, $community, 'memory', "$aruba.2.2.1.1.1.11" );
    foreach $row ( 1 .. $#{@{$data[2]}} ) {
	my $size = $data[2][$row];
	my $used = $data[3][$row];
	my $free = $data[4][$row];
	my $pc = int( $used * 100 / $size );
	$verbose && print "memory $pc% full\n";
	if ( $pc > 95 ) { 
	    push @crits, "memory $pc% full";
	    }
	elsif ( $pc > 90 ) { 
	    push @warns, "memory $pc% full";
	    }
	else  { 
	    #push @oks, "memory $pc% full";
	    }
	}



    # auth server table
    if ( 0 ) { 
	@data = walk_table2( $host, $community, 'auth-servers', "$aruba.2.2.1.8.1.1.1" );
	#print "dump ", Dumper( @data ), "\n";
	foreach $row ( sort keys %{$data[2]} ) {
	    my @a = split( /\./, $row );
	    shift @a;  # loose the length byte
	    my $name = pack( "C*", @a );
	    my $type = $data[2]{$row};
	    my $responsetime = $data[13]{$row};
	    $verbose && print "auth server $name avg response time $responsetime ms\n";
	    if ( $responsetime > 5000 ) { 
		push @crits, "auth server $name avg response time $responsetime ms";
		}
	    elsif ( $responsetime > 1000 ) { 
		push @warns, "auth server $name avg response time $responsetime ms";
		}
	    else  { 
		#push @oks, "auth server $name avg response time $responsetime ms";
		}
	    }
	}

    # fixme, do something with alarm mib

    # todo: check licences?  Our current ones say they never expire.


    }



sub check_ap { 
    my( $aptarget ) = @_;
    my( $i, $apref, $rc );

    $verbose && print "check_ap( $aptarget )\n";

    
    for $i ( 1 .. 3 ) { 
	if ( load_ap_cache() ) { 
	    last;
	    }
	if ( rebuild_ap_cache() ) { 
	    last;
	    }
	# maybe someone else is rebuilding it?
	usleep( 100000 );
	}
    for $i ( 1, 2 ) { 
	$apref = find_ap( $aptarget );
	if ( $apref && $apref->{ 'controller' } ) { 
	    $rc = scan_ap( $apref, $aptarget, 1 );
	    return if ( $rc == 1 );
	    }

	if ( ! rebuild_ap_cache() ) { 
	    last;
	    }
	sleep 1;
	}

    if ( ! $apref || $rc == 0 ) { 
	# still can't find it.  
	push @unknowns, "can't find controller for ap $aptarget";
	return;
	}
    if ( $rc == 2 ) { 
	# found it, but it's down
	$rc = scan_ap( $apref, $aptarget, 0 );
	}
    return $rc;
    }







# scan a specified ap on a specified controller
# return 0 if can't find it on this controller, or otherwise mismatched
# return 1 if found it and it's up
# return 2 if found it and it's down
sub scan_ap { 
    my( $apref, $aptarget, $no_down ) = @_;
    my( $controller, $apname, $apip, $apmac, $apmac_oid, $result, @oids,
	@apwlanrow, @apradiorow, $radio, %radios, $val, $oid2, $oid, 
	$col, $perf_util, $perf_clients );

    $verbose && print "scan_ap( $apref, $aptarget, $no_down )\n";

    $controller = $apref->{ controller };
    $apname = $apref->{ apname };
    $apip = $apref->{ apip };
    $apmac = $apref->{ apmac };
    $apmac_oid = $apref->{ apmac_oid };
    if ( $verbose ) { 
	print "$apname, $apip, $apmac, $apmac_oid, $controller\n";
	}

    my $sysDescr_oid = "$mib2.1.1.0";
    my $sysUpTime_oid = "$mib2.1.3.0";
    my $sysName_oid = "$mib2.1.5.0";
    @oids = ( $sysDescr_oid, $sysUpTime_oid, $sysName_oid );
    $result = snmp_get( $controller, $community, 'system', \@oids );
    my $sysName = $result->{ $sysName_oid };
    my $sysDescr = $result->{ $sysDescr_oid };
    my $sysUpTime = $result->{ $sysUpTime_oid };
    if ( $verbose ) { 
	print "sysName $sysName\n";
	print "sysDescr $sysDescr\n";
	print "sysUpTime $sysUpTime\n";
	}
    if ( ! $sysDescr ) { 
	#push @unknowns, "unable to get sysDescr";
	return 0;
	}
    elsif ( $sysDescr !~ m/ArubaOS/ ) { 
	#push @unknowns, "not an Aruba controller: $sysDescr";
	return 0;
	}

    # pick the stuff for this mac out of the AP table
    undef @oids;
    foreach $col ( 2 .. 31 ) { 
	$oid = "$aruba.2.2.1.5.2.1.4.1.$col.$apmac_oid";
	push @oids, $oid;
	}
    $result = snmp_get( $controller, $community, 'ap table', \@oids );
    foreach $oid ( keys %$result ) {
	if ( $oid =~ m/2\.2\.1\.5\.2\.1\.4\.1\.(\d+)\.$apmac_oid/ ) { 
	    $val = $result->{ $oid };
	    $verbose > 1 && print "$oid = $val\n";
	    $apwlanrow[ $1 ] = $val;
	    }
	}

    $apip = $apwlanrow[ 2 ];
    $apname = $apwlanrow[ 3 ];

    my @statuses = ( 'unknown', 'up', 'down' );

    if ( $verbose ) { 
	print "ap name $apname, ip $apip, mac $apmac\n";
	print "ap group ", $apwlanrow[ 4 ], "\n";
	#print "ap model ", $apwlanrow[ 5 ], "\n";   # as an index code
	print "ap serial ", $apwlanrow[ 6 ], "\n";
	print "ap num radios ", $apwlanrow[ 9 ], "\n";
	print "ap ipsec mode ", $apwlanrow[ 11 ], "\n";
	print "ap uptime ", $apwlanrow[ 12 ], "\n";
	print "ap model name ", $apwlanrow[ 13 ], "\n";   # as a string
	print "ap external antenna ", $apwlanrow[ 18 ], "\n";   # 1 not present, 2 enabled
	print "ap status ", $apwlanrow[ 19 ], " ", $statuses[ $apwlanrow[19] ], "\n";   # 1 up
	print "ap bootstraps ", $apwlanrow[ 20 ], "\n";   
	print "ap reboots ", $apwlanrow[ 21 ], "\n";   
	print "ap unprovisioned ", $apwlanrow[ 22 ], "\n";   # 1 up
	print "ap monitor mode ", $apwlanrow[ 23 ], "\n";   # 2 none
	print "ap mesh role ", $apwlanrow[ 31 ], "\n";   # 0 nonmesh
	}

    if ( $apname ne $aptarget && $apip ne $aptarget ) { 
	# We didn't get what we asked for.  
	# Maybe it got renamed and the cache is out of date?
	$verbose && print "name mismatch: want $aptarget, got $apname $apip\n";
	return 0; 
	}

    if ( $no_down ) { 
	if ( ! defined $apwlanrow[19] || $apwlanrow[19] == 0 ) { 
	    return 0;
	    }
	elsif ( $apwlanrow[19] == 2 ) { 
	    return 2;
	    }
	# else it's ok, keep going
	}

    if ( $apwlanrow[19] != 1 ) { 
	push @crits, $statuses[ $apwlanrow[19] ];
	}
    else { 
	push @oks, $statuses[ $apwlanrow[19] ];
	}

    push @oks, $apmac;
    #push @oks, "ip $apip";
    #push @oks, "name $apname";
    push @oks, "ap$apwlanrow[13]";
    push @oks, "sn $apwlanrow[6]";
    push @oks, "group $apwlanrow[4]";






    # pick the stuff for this mac out of the radio table

    # this doesn't work, it says the tables are empty...
    #undef @oids;
    #foreach $col ( 2 .. 14 ) { 
    #	$oid = "$aruba.2.2.1.5.2.1.5.1.$col.$apmac_oid";
    #	print "oid $oid\n";
    #	$result = snmp_walk( $controller, $community, 'radio', $oid );
    #
    #	foreach $oid2 ( keys %$result ) {
    #	    if ( $oid2 =~ m/2\.2\.1\.5\.2\.1\.5\.1\.$col\.$apmac_oid\.(\d+)$/ ) { 
    #		$val = $result->{ $oid2 };
    #		$verbose && print "$oid2 = $val\n";
    #		$apradiorow[ $col ][ $1 ] = $val;
    #		$radios{ $1 } = 1;
    #		}
    #	    }
    #	}

    foreach $radio ( 1 .. $apwlanrow[ 9 ] ) { 
	# Get each radio seperately, so an snmp not found for an inactive radio 
	# won't hide an active one.
	undef @oids;
	foreach $col ( 2 .. 14 ) { 
	    $oid = "$aruba.2.2.1.5.2.1.5.1.$col.$apmac_oid.$radio";
	    push @oids, $oid;
	    }
	$result = snmp_get( $controller, $community, "radio $radio", \@oids );
	foreach $oid ( keys %$result ) {
	    if ( $oid =~ m/2\.2\.1\.5\.2\.1\.5\.1\.(\d+)\.$apmac_oid\.$radio$/ ) { 
		$col = $1;
		$val = $result->{ $oid };
		$verbose > 1 && print "$oid = $val\n";
		$apradiorow[ $col ][ $radio ] = $val;
		}
	    }
	}

    my @radio_types = ( 'unknown', 'dot11a', 'dot11b', 'dot11g', 'dot11ag', 'wired' );

    $perf_util = $perf_clients = 0;
    foreach $radio ( 1 .. $apwlanrow[ 9 ] ) { 

	next if ( ! $apradiorow[ 2 ][$radio] || $apradiorow[ 2 ][$radio] eq "noSuchInstance" );

	if ( $verbose ) { 
	    print "radio $radio type ", $radio_types[ $apradiorow[ 2 ][$radio] ], "\n";
	    print "radio $radio channel ", $apradiorow[ 3 ][$radio], "\n";
	    printf "radio $radio tx power %0.1f dBm\n", $apradiorow[ 4 ][$radio] / 2;
	    print "radio $radio mode ", $apradiorow[ 5 ][$radio], "\n";   # as an index code
	    print "radio $radio utilization ", $apradiorow[ 6 ][$radio], "\n";
	    print "radio $radio num associated clients ", $apradiorow[ 7 ][$radio], "\n";
	    print "radio $radio num monitored clients ", $apradiorow[ 8 ][$radio], "\n";
	    print "radio $radio num active bssids ", $apradiorow[ 9 ][$radio], "\n";
	    print "radio $radio num monitored bssids ", $apradiorow[ 10 ][$radio], "\n";
	    print "radio $radio bearing ", $apradiorow[ 11 ][$radio], "\n";   # as a string
	    print "radio $radio tilt angle ", $apradiorow[ 12 ][$radio], "\n";   # as a string
	    # 13 ?
	    # 14 ?
	    # 15 channel?
	    }

	push @oks, sprintf( "%s ch%d %2.1fdBm %d%%util %dclients", 
	    $radio_types[ $apradiorow[ 2 ][$radio] ],
	    $apradiorow[ 3 ][$radio],
	    $apradiorow[ 4 ][$radio] / 2,
	    $apradiorow[ 6 ][$radio],
	    $apradiorow[ 7 ][$radio] );

	$perf_util = $apradiorow[ 6 ][$radio] if ( $apradiorow[ 6 ][$radio] > $perf_util );
	$perf_clients += $apradiorow[ 7 ][$radio];
	}

    push @perfdata, sprintf "util=%d", $perf_util;
    push @perfdata, sprintf "clients=%d", $perf_clients;

    # fixme, do something with alarm mib

    push @oks, "controller $sysName";

    return 1;
    }



# search the ap_cache for an ap ip or name
# return a reference to it's entry, or 0 on failure
sub find_ap { 
    my( $ap ) = @_;
    my( $apmac_oid );

    $verbose && print "find_ap( $ap )\n";

    if ( $ap =~ m/^[\d\.]{7,15}$/ ) { 
	# ip address 
	foreach $apmac_oid ( keys %$ap_cache ) { 
	    if ( exists $ap_cache->{ $apmac_oid }{ apip } 
		    && $ap_cache->{ $apmac_oid }{ apip } eq $ap ) { 
		$verbose && print "found $ap = $apmac_oid\n";
		return \%{$ap_cache->{ $apmac_oid }};
		}
	    }
	}
    else { 
	# ap name
	foreach $apmac_oid ( keys %$ap_cache ) { 
	    if ( exists $ap_cache->{ $apmac_oid }{ apname } 
		    && $ap_cache->{ $apmac_oid }{ apname } eq $ap ) { 
		$verbose && print "found $ap = $apmac_oid\n";
		return \%{$ap_cache->{ $apmac_oid }};
		}
	    }
	}

    return 0;   # can't find it
    }



# load ap cache hash from storable file 
# on success return $ap_cache 
# on failure return false
sub load_ap_cache { 

    $verbose && print "load_ap_cache()\n";

    if ( ( ! -f $cache_file ) || ( -s $cache_file ) < 100 ) { 
	$verbose && warn "cache file is invalid, $cache_file";
	return 0;
	}

    #open( lockH, $cache_file ) || return 0;
    #flock( lockH, LOCK_SH ) || return 0;

    $ap_cache = retrieve( $cache_file );

    #flock( lockH, LOCK_UN ) || return 0;
    #close( lockH );

    return $ap_cache;
    }






# rebuild the cache of aps
# return 1 success, 0 failure
sub rebuild_ap_cache { 
    my( $sysDescr, $result, $oid, @controllers, $controller, $rc, $apmac_oid, 
	$apmac, $age );

    $verbose && print "rebuild_ap_cache()\n";

    if ( -f $cache_file ) { 
	# can't use -M in embedded perl nagios, it computes file age 
	# relative to when nagios demon started.  
	my( $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, $size,
	    $atime, $mtime, $ctime, $blksize, $blocks ) = stat( _ );
	$age = time() - $mtime;
	if ( $age < $cache_holddown_time && ! $force ) { 
	    $verbose && print "cache file only $age seconds old, not rebuilding\n"; 
	    return 0;
	    }
	}

    if ( ! open( lockH, "+>>$cache_file.new" ) ) { 
	$verbose && warn "can't open new cache file to lock it: $!\n";
	push @warns, "can't open new cache file to lock it: $!";
	return 0;
	}
    if ( ! flock( lockH, LOCK_EX | LOCK_NB ) ) { 
	$verbose && warn "can't lock new cache file: $!\n";
	push @warns, "can't lock new cache file: $!";
	return 0;
	}

    $sysDescr = snmp_get_one( $host, $community, 'sysDescr', "$mib2.1.1.0" );
    if ( ! $sysDescr ) { 
	push @unknowns, "unable to get sysDescr";
	flock( lockH, LOCK_UN );
	close( lockH );
	return;
	}
    $verbose && print "sysDescr $sysDescr\n";
    if ( $sysDescr !~ m/ArubaOS/ ) { 
	push @unknowns, "not an Aruba controller: $sysDescr";
	flock( lockH, LOCK_UN );
	close( lockH );
	return;
	}

    $result = snmp_walk( $host, $community, 'switchListSwitchRole', "$aruba.2.2.1.1.1.6.1.2" );
    foreach $oid ( keys %$result ) {
	if ( $oid =~ m/6\.1\.2\.(\d+\.\d+\.\d+\.\d+)$/ ) {
	    push @controllers, $1;
	    }
	}

    $ap_cache = {}; 
    foreach $controller ( @controllers ) { 
	next if ( ! $controller );
	rebuild_ap_cache_per_controller( $controller );
	}

    foreach $apmac_oid ( sort keys %$ap_cache ) { 
	$ap_cache->{ $apmac_oid }{ 'apmac_oid' } = $apmac_oid;
	$apmac = sprintf( "%02x:%02x:%02x:%02x:%02x:%02x", 
	    split( /\./, $apmac_oid ) );
	$ap_cache->{ $apmac_oid }{ 'apmac' } = $apmac;
	if ( $verbose  ) { 
	    printf "ap mac %s, name %s, ip %s, controller %s, status %s\n",
		$apmac,
		$ap_cache->{ $apmac_oid }{ 'apname' },
		$ap_cache->{ $apmac_oid }{ 'apip' },
		$ap_cache->{ $apmac_oid }{ 'controller' },
		$ap_cache->{ $apmac_oid }{ 'status' };
	    }
	}

    if ( ! store $ap_cache, "$cache_file.new" ) { 
	$verbose && warn "can't store to new cache file: $!\n";
	push @warns, "can't store to new cache file: $!";
	flock( lockH, LOCK_UN );
	close( lockH );
	return 0;
	}

    # rename is atomic 
    # Won't disturb other instances of this script that already # have it open.  
    if ( ! rename( "$cache_file.new", $cache_file ) ) { 
	$verbose && warn "unable to rename new cache file: $!\n";
	push @warns, "unable to rename new cache file: $!";
	flock( lockH, LOCK_UN );
	close( lockH );
	return 0;
	}

    flock( lockH, LOCK_UN );
    close( lockH );
    return 1;   # success
    }






sub rebuild_ap_cache_per_controller { 
    my( $controller ) = @_;
    my( $result, @oids, $val, $oid, $apmac_oid, $status );

    $verbose && print "rebuild_ap_cache_per_controller( $controller )\n";

    my $sysDescr_oid = "$mib2.1.1.0";
    my $sysUpTime_oid = "$mib2.1.3.0";
    my $sysName_oid = "$mib2.1.5.0";
    @oids = ( $sysDescr_oid, $sysUpTime_oid, $sysName_oid );
    $result = snmp_get( $controller, $community, 'system', \@oids );
    my $sysName = $result->{ $sysName_oid };
    my $sysDescr = $result->{ $sysDescr_oid };
    my $sysUpTime = $result->{ $sysUpTime_oid };
    if ( $verbose ) { 
	print "sysName $sysName\n";
	print "sysDescr $sysDescr\n";
	print "sysUpTime $sysUpTime\n";
	}
    if ( ! $sysDescr ) { 
	#push @unknowns, "unable to get sysDescr";
	return 0;
	}
    elsif ( $sysDescr !~ m/ArubaOS/ ) { 
	#push @unknowns, "not an Aruba controller: $sysDescr";
	return 0;
	}

    my @statuses = ( 'unknown', 'up', 'down' );


    # wlsxWlanAPTable.wlsxWlanAPEntry.wlanAPIpAddress
    $result = snmp_walk( $controller, $community, 'wlanAPIpAddress', 
	"$aruba.2.2.1.5.2.1.4.1.2" );
    foreach $oid ( keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	if ( $oid =~ m/\.(\d+\.\d+\.\d+\.\d+\.\d+\.\d+)$/ ) {
	    $apmac_oid = $1;
	    $ap_cache->{ $apmac_oid }{ 'apip' } = $val;
	    }
	}

    # wlsxWlanAPTable.wlsxWlanAPEntry.wlanAPName
    $result = snmp_walk( $controller, $community, 'wlanAPName', "$aruba.2.2.1.5.2.1.4.1.3" );
    foreach $oid ( keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	if ( $oid =~ m/\.(\d+\.\d+\.\d+\.\d+\.\d+\.\d+)$/ ) {
	    $apmac_oid = $1;
	    $ap_cache->{ $apmac_oid }{ 'apname' } = $val;
	    }
	}

    # wlsxWlanAPTable.wlsxWlanAPEntry.wlanAPStatus
    $result = snmp_walk( $controller, $community, 'wlanAPStatus', 
	"$aruba.2.2.1.5.2.1.4.1.19" );
    foreach $oid ( keys %$result ) {
	$val = $result->{ $oid };
	$status = $statuses[ $val ];
	$verbose > 1 && print "$oid = $val $status\n";
	if ( $oid =~ m/\.(\d+\.\d+\.\d+\.\d+\.\d+\.\d+)$/ ) {
	    $apmac_oid = $1;
	    # we want to save the controller that sees the ap as 'up'
	    if ( ! exists $ap_cache->{ $apmac_oid }{ 'status' } 
		    || $ap_cache->{ $apmac_oid }{ 'status' } ne 'up' ) {
		$ap_cache->{ $apmac_oid }{ 'controller' } = $controller;
		$ap_cache->{ $apmac_oid }{ 'status' } = $status;
		}
	    }
	}

    return 1;
    }









# for tables with simple 1 integer indecies, ie most normal ones
# Returns an array of arrays: data[ column ][ row ]
sub walk_table {
    my( $dev, $community, $name, $baseoid ) = @_;
    my( $result, $rows, $oid, $val, $col, $row, @data );

    $verbose && print "walk_table $dev $name\n";

    $result = snmp_walk( $dev, $community, $name, $baseoid ) || return undef;

    $rows = 0;
    foreach $oid ( keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	next if ( $val eq 'endOfMibView' );
	if ( $oid =~ m/.*\.(\d+)\.(\d+)$/ ) {
	    $col = $1; $row = $2;
	    $data[$col][$row] = $val;
	    $rows = $row if ( $row > $rows );
	    }
	}

    if ( ! $rows ) {
        push @unknowns, "no rows in $name table on $dev";
	return undef;
        }

    return @data;
    }






# for tables with complex indecies, like macaddrs or ips
# Expectes the next octet of the oid after the baseoid to be the column number,
# and anything after that to be the index/row.   
# So usually have to give this a baseoid with a final 1 for the "entry" level, 
# not just the "table" level oid.
# Returns an array of hashes: data[ column ]{ row }
sub walk_table2 {
    my( $dev, $community, $name, $baseoid ) = @_;
    my( $result, %rows, $nrows, $oid, $val, $col, $row, @data );

    $verbose && print "walk_table2 $dev $name\n";

    $result = snmp_walk( $dev, $community, $name, $baseoid ) || return undef;

    foreach $oid ( keys %$result ) {
	$val = $result->{ $oid };
	$verbose > 1 && print "$oid = $val\n";
	next if ( $val eq 'endOfMibView' );
	if ( $oid =~ m/$baseoid\.(\d+)\.([\d\.]+)$/ ) {
	    $col = $1; $row = $2;
	    $rows{ $row } = 1;
	    $data[$col]{$row} = $val;
	    }
	}

    $nrows = scalar keys %rows;
    if ( ! $nrows ) {
        push @unknowns, "no rows in $name table on $dev";
	return undef;
        }

    return @data;
    }





sub snmp_walk {
    my( $dev, $community, $name, $baseoid ) = @_;
    my( $session, $result );

    $verbose && print "snmp_walk $dev $name\n";

    $session = snmp_session( $dev, $community );
    $result = $session->get_table( -baseoid => $baseoid );
    #print "session error ", $session->error(), "\n";
    if ( ! defined( $result )
            && $session->error() !~ m/Requested table is empty/ ) {
	#warn sprintf( "error walking $name table on %s: %s",
        #    $session->hostname, $session->error() );
        #push @unknowns, sprintf( "error walking $name table on %s: %s",
        #    $session->hostname, $session->error() );
	return undef;
        }
    return $result;
    }





sub snmp_get_one {
    my( $dev, $community, $name, $oid ) = @_;
    my( $session, @oids, $result, $oid2, $val );

    $verbose && print "snmp_get $dev $name\n";
    $session = snmp_session( $dev, $community );
    @oids = ( $oid );
    $result = $session->get_request( -varbindlist => \@oids );
    if ( ! defined( $result ) ) {
        #warn sprintf "error getting %s from %s: %s\n",
        #    $name, $session->hostname, $session->error();
        return undef;
        }

    if ( ! exists $result->{ $oid } ) {
        #warn "snmp_get error: requested $name oid $oid not in response\n";
        return undef;
        }
    return $result->{ $oid };
    }




sub snmp_get {
    my( $dev, $community, $name, $oids ) = @_;
    my( $session, $result, $oid );

    $verbose && print "snmp_get $dev $name\n";
    $session = snmp_session( $dev, $community );
    $result = $session->get_request( -varbindlist => $oids );
    if ( ! defined( $result ) ) {
        #warn sprintf "error getting %s from %s: %s\n",
        #    $name, $session->hostname, $session->error();
        return undef;
        }

    # clean the noSuchObject errors out of the result
    foreach $oid ( keys %{$result} ) {
        if ( $result->{ $oid } eq 'noSuchObject' ) {
            delete $result->{ $oid };
            }
        }

    return $result;
    }



sub snmp_session { 
    my( $dev, $community ) = @_;
    my( $ip, $session, $error );
    
    if ( $dev =~ m/^\d[\d\.]+$/ ) { 
        $ip = $dev;
        }
    else { 
        $ip = name2ip( $dev );
        }

    if ( exists $snmp_sessions{ "$ip,$community" } ) { 
        return $snmp_sessions{ "$ip,$community" };
        }

    ( $session, $error ) = Net::SNMP->session(
        -version => $use_snmpv2c ? 'snmpv2c' : 'snmpv1',
        -hostname => $ip,
        -community => $community,
        -timeout => 5.0,
        #-debug => 0x02
        );
    if ( ! defined( $session ) ) {
        push @unknowns, "snmp setup error: $error\n";
        return undef;
        }
    $session->translate( [ '-octetstring' => 0 ] );

    $snmp_sessions{ "$ip,$community" } = $session;
    return $session;
    }



sub name2ip { 
    my( $name ) = @_;
    my( $ip, $a, $b, $c, $d );

    if ( defined( $name2ip{ $name } ) ) {  
        $ip = $name2ip{ $name };
        }
    elsif ( $name =~ m/^\s*(\d+\.\d+\.\d+\.\d+)\s*$/ ) { 
        $ip = $1;
        $name2ip{ $name } = $ip;
        $name2ip{ $1 } = $ip;
        }
    else { 
        $ip = gethostbyname( $name );
        if ( ! $ip ) { 
            return undef;
            }
        ($a,$b,$c,$d) = unpack( 'C4', $ip );
        $ip = "$a.$b.$c.$d";
        $name2ip{ $name } = $ip;
        }
    return $ip;
    }

