#!/usr/bin/perl

our $VERSION = "0.0.0";
$VERSION = eval $VERSION;

use Modern::Perl;
use Getopt::Long 2.39;
use File::Basename qw(basename);
use Net::SNMP      qw(:snmp);

# application context
my $app = { };

# wireless oids (see ftp://ftp.cisco.com/pub/mibs/v2/AIRESPACE-WIRELESS-MIB.my)
$app->{snmp}{oid}{bsnDot11EssSsid}                   = "1.3.6.1.4.1.14179.2.1.1.1.2";
$app->{snmp}{oid}{bsnDot11EssNumberOfMobileStations} = "1.3.6.1.4.1.14179.2.1.1.1.38";
$app->{snmp}{oid}{bsnAPName}                         = "1.3.6.1.4.1.14179.2.2.1.1.3";
$app->{snmp}{oid}{bsnAPIfLoadNumOfClients}           = "1.3.6.1.4.1.14179.2.2.13.1.4";
	
# anonymous pod2usage sub
my $pod2usage = sub {
    require Pod::Usage;
	Pod::Usage::pod2usage(@_);
};

BEGIN {
    # set alarm to kill script if it takes too long to finish
    $SIG{ALRM} = sub { die "" };
    alarm 60;
}

END {
    say "ERROR: " . $app->{error} if $app->{error};

    # cancel alarm
    alarm 0;
}


### Main Processing Section ###

MAIN:
{
    # parse command line
    parse_cmdline(@ARGV);

    # check hostname
	my $host  = $app->{host}  = shift @{$app->{args}};
    $pod2usage->( -message => "Missing hostname!\n"     ) unless $host;
	
    # check query
	my $query = $app->{query} = shift @{$app->{args}};
    $pod2usage->( -message => "Missing query!\n"        ) unless $query;
    $pod2usage->( -message => "Unknown query: $query\n" ) unless $query eq "associations";

    $query .= " " . shift @{$app->{args}} if @{$app->{args}};
    $pod2usage->( -message => "Unknown query: $query\n" ) unless $query eq "associations ssid" or $query eq "associations building";

    # check snmp options
    $pod2usage->( -message => "Missing SNMP version!\n"     ) unless $app->{opts}{'snmp-version'};
    $pod2usage->( -message => "Unsupported SNMP version!\n" ) unless $app->{opts}{'snmp-version'} eq "1" or $app->{opts}{'snmp-version'} eq "2";
    $pod2usage->( -message => "Missing SNMP community!\n"   ) unless $app->{opts}{'snmp-community'};

    # check devices
    $pod2usage->( -message => "Missing query argument(s)!\n" ) if scalar @{$app->{args}} == 0;

    # open snmp session
    open_snmp_session($host, $app->{opts}{'snmp-version'}, $app->{opts}{'snmp-community'});
    
    if ($query eq "associations ssid") {
        # get associations per ssid
        my $ssid = get_associations_per_ssid($app->{args});

        # print cacti string
        print hash2cacti($ssid);
    }

    if ($query eq "associations building") {
        # get associations per building
        my $building = get_associations_per_building($app->{args});

        # print cacti string
        print hash2cacti($building);
    }

    # close snmp session
    close_snmp_session();

#    # open snmp session
#    open_snmp_session();
#
#    # get statistics
#    get_associations_per_ssid()     if $QUERY eq "associations ssid";
#    get_associations_per_building() if $QUERY eq "associations building";
#
#    # close snmp session
#    close_snmp_session();
#
#    # return cacti string and exit
#    print_cacti_string();
#    exit(0);
}


### Subroutines ###

#sub parse_cli_input {
#    # Parse options
#    GetOptions(\%OPT, "v|snmp-version=s", "c|snmp-community=s", "V|version", "help|?", "debug") or pod2usage(-verbose => 0);
#
#    # Check options
#    print_version()           if $OPT{V};
#    pod2usage(-verbose => -1) if $OPT{help};
#    $DEBUG = 1                if $OPT{debug};
#
#    # Get arguments
#    $HOSTNAME   = shift @ARGV;
#    $QUERY      = shift @ARGV;
#
#    # Check arguments
#    pod2usage(-verbose => 0, -message => "Missing hostname!\n")         unless $HOSTNAME;
#    pod2usage(-verbose => 0, -message => "Missing query!\n")            unless $QUERY;
#    pod2usage(-verbose => 0, -message => "Unknown query: $QUERY\n")     unless $QUERY eq "associations";
#
#        $QUERY .= " " . shift @ARGV if @ARGV;
#    pod2usage(-verbose => 0, -message => "Unknown query: $QUERY\n")     unless $QUERY eq "associations ssid" or $QUERY eq "associations building";
#    $QUERY_ARGS = \@ARGV;
#
#    # Check SNMP options
#    pod2usage(-verbose => 0, -message => "Missing SNMP version!\n")     unless $OPT{v};
#    pod2usage(-verbose => 0, -message => "Unsupported SNMP version!\n") unless $OPT{v} eq "1" or $OPT{v} eq "2";
#    pod2usage(-verbose => 0, -message => "Missing SNMP community!\n")   unless $OPT{c};
#}

sub parse_cmdline {
    my @argv = @_;

    # option parser
    my $parser = Getopt::Long::Parser->new;
    $parser->configure('no_ignore_case');

    # option specifications
    my %opts;
    my @specs = (
        "debug",                    # optional,  debug
        "help|h",                   # optional,  help info
        "manual|m",                 # optional,  complete manual
        "version|V",                # optional,  version info
        "snmp-version|v=s",         # mandatory, snmp version
        "snmp-community|c=s",       # mandatory, snmp community string
    );
	
    # parse options
    $pod2usage->(
        -verbose => 0,
        -exitval => 2,
        -message => " ",
    ) unless $parser->getoptionsfromarray(\@argv, \%opts, @specs);

    print_version() if $opts{version};
    print_manual()  if $opts{manual};
    print_help()    if $opts{help};

    # store args and opts in context
    $app->{args} = \@argv;
    $app->{opts} = \%opts;

    return 1;
};

sub open_snmp_session {
    my ($host, $version, $community) = @_;
    debug("Opening SNMP session to host $host");

    ($app->{snmp}{session}, $app->{error}) = Net::SNMP->session(
        -hostname  => $host,
        -version   => $version,
        -community => $community,
    );

    exit(1) unless ($app->{snmp}{session});
}

sub close_snmp_session {
    debug("Closing SNMP session");

    $app->{snmp}{session}->close;
}

sub get_associations_per_ssid {
    my $ssids = shift;
    debug("Retreiving associations per ssid");

    # 'other' ssid and total
    my %associations = ( other => 0, total => 0 );

    # wanted ssid's (given by cli)
    foreach my $ssid (@$ssids) {
        $associations{$ssid} = 0;
    }

    my $wlans = list_wlans();
    foreach my $wlan (keys %{$wlans}) {
        my $index = $wlans->{$wlan};
        my $client_count = get_wlan_client_count($index);

        debug("Wlan $wlan counts $client_count clients");
        if (exists $associations{$wlan}) {
            $associations{$wlan} += $client_count;
        } else {
            $associations{'other'} += $client_count;
        }
        $associations{'total'} += $client_count;
    }

    return \%associations;
}


sub get_associations_per_building {
    my $buildings = shift;
    debug("Retreiving associations per building");

    # 'other' building and total
    my %associations = ( other => 0, total => 0);

    # wanted buidings
    foreach my $building (@$buildings) {
        $associations{$building} = 0;
    }

    my $aps = list_accesspoints();
    foreach my $ap (keys %{$aps}) {
        my $index = $aps->{$ap};
        my $client_count = get_ap_client_count($index);

        my $building = get_building_name($ap);
        # debug("Accesspoint $ap in building $building counts $client_count clients");
        if (exists $associations{$building}) {
            $associations{$building} += $client_count;
        } else {
            $associations{'other'} += $client_count;
        }
        $associations{'total'} += $client_count;
    }

    return \%associations;
}

sub list_wlans {
    my %ssid;
    debug("Query configured ssids");

    my $snmp = $app->{snmp};
    my $ssid_table = $snmp->{session}->get_table($snmp->{oid}{bsnDot11EssSsid});

    if (not defined $ssid_table) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    my @ssid_oids = $snmp->{session}->var_bind_names;
    foreach my $ssid_oid (@ssid_oids) {
        my $snmp_data = $snmp->{session}->get_request($ssid_oid);

        if (not defined $snmp_data) {
            $app->{error} = $snmp->{session}->error();
            exit(1);
        }

        if (oid_base_match($snmp->{oid}{bsnDot11EssSsid}), $ssid_oid) {
            my $ssid_name = $snmp_data->{$ssid_oid};
            my $ssid_index = substr($ssid_oid, length($snmp->{oid}{bsnDot11EssSsid}) + 1, length($ssid_oid));
            debug("Found ssid $ssid_name with index $ssid_index");
            $ssid{$ssid_name} = $ssid_index;
        }
    }

    return \%ssid;
}

sub list_accesspoints {
    my %ap;

    my $snmp = $app->{snmp};
    my $ssid_table = $snmp->{session}->get_table($snmp->{oid}{bsnAPName});

    if (not defined $ssid_table) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    my @ap_oids = $snmp->{session}->var_bind_names;
    foreach my $ap_oid (@ap_oids) {
        my $snmp_data = $snmp->{session}->get_request($ap_oid);

        if (not defined $snmp_data) {
            $app->{error} = $snmp->{session}->error();
            exit(1);
        }

        if (oid_base_match($snmp->{oid}{bsnAPName}), $ap_oid) {
            my $ap_name = $snmp_data->{$ap_oid};
            my $ap_index = substr($ap_oid, length($snmp->{oid}{bsnAPName}) + 1, length($ap_oid));
            # debug("Found ap $ap_name with index $ap_index");
            $ap{$ap_name} = $ap_index;
        }
    }

    return \%ap;
}

sub get_wlan_client_count {
    my $index = shift;

    my $snmp = $app->{snmp};
    my $client_count_oid = $snmp->{oid}{bsnDot11EssNumberOfMobileStations} . ".$index";

    my $snmp_data = $snmp->{session}->get_request($client_count_oid);
    if (not defined $snmp_data) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    return $snmp_data->{$client_count_oid};
}

sub get_ap_client_count {
    my $index = shift;
    my $count = 0;

    my $snmp = $app->{snmp};
    my $client_count_oid = $snmp->{oid}{bsnAPIfLoadNumOfClients} . ".$index";
    my $radio_table = $snmp->{session}->get_table($client_count_oid);

    if (not defined $radio_table) {
        $app->{error} = $snmp->{session}->error();
        exit(1);
    }

    my @radio_oids = $snmp->{session}->var_bind_names;
    foreach my $radio_oid (@radio_oids) {
        my $snmp_data = $snmp->{session}->get_request($radio_oid);

        if (not defined $snmp_data) {
            $app->{error} = $snmp->{session}->error();
            exit(1);
        }

        if (oid_base_match($client_count_oid), $radio_oid) {
            $count += $snmp_data->{$radio_oid};
        }
    }

    return $count;
}

sub get_building_name {
    my $ap_name = lc(shift);
    # debug("Get building from ap with name $ap_name");

    my $building;
    if ( $ap_name =~ /^ap-(.*?)(\d+)([a-z])/i && index($ap_name, '.') == -1 && ($ap_name =~ tr/-//) == 1) {
        $building = $1;
        # strip 'z' (college room) from building name
        $building = $1 if ($building =~ /([a-z])(.*)z$/);
    } else {
        $building = 'other';
    }

    return $building;
}

sub hash2cacti {
    my $href = shift;
    my $result;

    $result = join(" ", map { "$_:$href->{$_}" } sort keys %$href);
	$result = "CACTI | $result\n" if $app->{opts}{debug};

    return $result;
}

sub print_version {
    say STDERR basename($0) . " v" . main->VERSION();
    exit(0);
}

sub print_manual {
    alarm 0;
    $pod2usage->(
        -verbose => 2,
        -exitval => 1,
    );
};

sub print_help {
    $pod2usage->(
        -verbose => 0,
        -exitval => 1,
    );
};

sub debug {
    my $string = shift;
    say "DEBUG | $string" if $app->{opts}{debug};
}

__END__


=head1 NAME

wlc2cacti - Query Cisco Wireless LAN Controller statistics for Cacti

=head1 SYNOPSIS

B<wlc2cacti> {snmp options} hostname <query> [options]

=over 2

=item SNMP Options:

  -v, --snmp-version { 1 | 2 }        SNMP version
  -c, --snmp-community <community>    SNMP community

=item Query:

  associations ssid     {<name> [<name>]...}    Clients per wlan
  associations building {<name> [<name>]...}    Clients per building

=item Options:

  -h --help       Help info
  -m --manual     Manpages
  -V --version    Version info
  -d --debug      Debugging

=back

=head1 DESCRIPTION

This program queries a Wireless LAN Controller for client statistics. The number of clients are grouped by one ore more ssids or buildings. The output is formatted to use with Cacti.

=head1 ARGUMENTS

=over 4

=item B<hostname>

Management address of Cisco WLC

=back

=head1 SNMP OPTIONS

=over 4

=item B<-v,  --snmp-version>

Specifies SNMP version

=item B<-c,  --snmp-community>

Specifies SNMP community

=back

=head1 QUERY

=over 4

=item B<associations ssid <name> [<name>]...>

Returns the number associations per ssid. Results are grouped by the given name(s). Results which don't match any given name will be grouped by the name 'other'.

=item B<associations building <name> [<name>]...>

Returns the numbers of associations per building. Results are grouped by the given name(s). Results which don't match any given name will be grouped by the name 'other'.

=back

=head1 OPTIONS

=over 4

=item B<-h, --help>

Displays a brief summary of asa2cacti's options.

=item B<-m, --manual>

Displays asa2cacti's full documentation.

=item B<-V, --version>

Display the version of asa2cacti.

=item B<-d, --debug>

Displays debug information.

=back

=head1 EXAMPLES

wlc2cacti wlc.localdomain -v 2 -c public associations ssid eduroam

wlc2cacti -v 2 -c public wlc.localdomain associations building a b c

=head1 AUTHOR

Jurgen van den Hurk, E<lt>jvdhurk@tilburguniversity.eduE<gt>

=head1 LICENSE AND COPYRIGHT

Copyright 2017 Tilburg University. All rights reserved.

This program is free software; you may redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut




perl wlc2cacti wlc.localdomain -v 2 -c public associations ssid eduroam Noc-Test TN-Wireless -d
perl wlc2cacti uvt-wlc.uvt.nl -v 2 -c hkbu4kma associations building

perl wlc2cacti uvt-wlc.uvt.nl -v 2 -c hkbu4kma associations building a c d dp e f g i k l m nkr o os p s r rp rt nv ne t tu w v z



