#!/usr/bin/perl -w
use strict;
# $Id: alertgrid_snmp 28 2014-11-25 15:21:20Z abalama $

=head1 NAME

alertgrid_snmp - MonM alertgrid SNMP getter

=head1 VERSION

Version 1.00

=head1 SYNOPSIS

    alertgrid_snmp [-dSX] [-v VERSION] [-t MICROSECS] [-x EXPIRES] [-s HOST] [-c COMMUNITY] COMMAND ARGUMENTS
    
    alertgrid_snmp [OPTIONS] get OID
    
    alertgrid_snmp [OPTIONS] table TABLE-OID
    
    alertgrid_snmp [OPTIONS] resources [RESOURCE]
    
    alertgrid_snmp [OPTIONS] traffic [if1 if2 ... if#]

    Type alertgrid_snmp -H for more information

=head1 DESCRIPTION

Getter data from SNMP server for alertgrid system

=head2 COMMANDS

=over

=item B<get>

Getting SNMP data by OID

=item B<table>

Getting SNMP datatable by TABLE-OID

=item B<resources>

Getting HDD, CPU, MEM and SWAP statistic data

=item B<traffic>

Getting I/O traffic statistic in octets. All if-indexes for this command must be getting 
from "alertgrid table ifTable" command

=back

=head2 OPTIONS

=over

=item B<-d, --debug>

Enable debug mode

=item B<-S, --strict>

Enable strict mode. All OIDs must be only as is long-format strings

    alertgrid_snmp -S get .1.3.6.1.2.1.1.1.0

=item B<-X, --noxml>

Output all data "as is"

=item B<-v VERSION-SNMP, --version=VERSION-SNMP>

Version of SNMP (default = 2c)

=item B<-t MICROSECS, --timeout=MICROSECS>

Timeout of SNMP requests in microseconds (default = 1000000)

=item B<-x EXPIRES, --expires=EXPIRES>

Time of life data in special "expires"-format (+5m as default).

See L<MonM::AlertGrid/expire_calc>

=item B<-s HOST, --host=HOST>

Host of SNMP (default = localhost)

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

Community name (default = public)

=back

=head1 EXAMPLES

    alertgrid_snmp -c mnsdesktop get .1.3.6.1.2.1.1.1
    alertgrid_snmp -c mnsdesktop table ifTable

=head1 DEPENDENCES

L<CTK::Util>, L<SNMP> as Net-SNMP (see down)

=head1 REQUIREMENTS

=over

=item Net-SNMP

To use this module, you must have Net-SNMP installed on your system.
More specifically you need the Perl modules that come with it.

DO NOT INSTALL SNMP or Net::SNMP from CPAN!

The SNMP module is matched to an install of net-snmp, and must be installed
from the net-snmp source tree.

The Perl module C<SNMP> is found inside the net-snmp distribution.  Go to the
F<perl/> directory of the distribution to install it, or run
C<./configure --with-perl-modules> from the top directory of the net-snmp
distribution.

Net-SNMP can be found at http://net-snmp.sourceforge.net

Version 5.3.2 or greater is recommended.

B<Redhat Users>: Some versions that come with certain versions of
Redhat/Fedora don't have the Perl library installed.  Uninstall the RPM and
install by hand.

=back

=head1 TODO

=over

=item Multiple selection

Add supporting of multiple selection SMTP data

=back

=head1 AUTHOR

Serz Minus (Lepenkov Sergey) L<http://www.serzik.com> E<lt>minus@mail333.comE<gt>.

=head1 COPYRIGHT

Copyright (C) 1998-2014 D&D Corporation. All Rights Reserved

=head1 LICENSE

This program is distributed under the GNU GPL v3.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

See C<LICENSE> file

=cut

use Module::Loaded;
use Try::Tiny;
use XML::Simple;
use CTK::Util;

use Getopt::Long;
use Pod::Usage;
use constant {
        TIMEOUT     => 1000000,
        RETRIES     => 5,
        HOST        => 'localhost',
        PORT        => 161,
        COMMUNITY   => 'public',
        VER         => '2c',
        EXPIRES     => '+5m',
        TESTOBJ     => '.1.3.6.1.2.1.1.3.0',
        UNITS       => {
                bytes   => 1,
                b       => 1,
                kbytes  => 1024,
                kb      => 1024,
                mbytes  => 1024**2,
                mb      => 1024**2,
                gbytes  => 1024**3,
                gb      => 1024**3,
                tbytes  => 1024**4,
                tb      => 1024**4,
            },
    };

use vars qw/$VERSION/;
$VERSION = '1.00';
BEGIN { sub exception{ print "ERROR"; die sprintf("[%s GMT] ERROR %s",scalar(gmtime(time())),(@_ ? @_ : ''))} }
Getopt::Long::Configure("bundling");
my %OPT;
GetOptions(\%OPT,
    "help|usage|h|?",
    "man|m|H",
    "debug|d",      # Debug mode: 0/1
    "strict|S",     # Strict mode: 0/1
    "noxml|X",      # Output as text
    
    "version|v=s",          # Version (default = 2c)
    "timeout|time|t=i",     # Timeout (default = 1000000)
    "expires|x=s",          # Expires (+5m as default)
    "host|server|s=s",      # Host SNMT (default = localhost)
    "comm|community|c=s",   # Community (default = public)
    
) || pod2usage(-exitval => 1, -verbose => 0);
pod2usage(-exitval => 0, -verbose => 0) if $OPT{help};
pod2usage(-exitval => 0, -verbose => 2) if $OPT{man};
my @args = @ARGV ? @ARGV : (); # Arguments
my $cmd = lc(shift(@args) || 'get');

my $debug   = $OPT{debug} || 0;
my $strictm = $OPT{strict} || 0;
my $host    = $OPT{host} || HOST;
my $timeout = $OPT{timeout} || TIMEOUT;
my $comm    = $OPT{comm} || COMMUNITY;
my $ver     = $OPT{version} || VER;
my $expires = $OPT{expires} || EXPIRES;

# Load module Net-SNMP
try {
    require SNMP;
    my $SNMPV = SNMP->VERSION;
    die "VERSION need 5.3.2 or more" if $SNMPV < 5.0302;
} catch {
    warn $_ if $_ =~ /VERSION/;
    warn "Module SNMP 5.3.2 is not loaded. Please install Net-SNMP from official site of Net-SNMP project\n";
    pod2usage(-exitval => 0, -verbose => 99, -sections => 'REQUIREMENTS');
};
pod2usage(-exitval => 0, -verbose => 99, -sections => 'REQUIREMENTS') unless (is_loaded('SNMP'));

# Loading all MIBs
$ENV{'MIBS'} = 'ALL';
&SNMP::initMib();

# Create SNMP::Session
my $snmp = new SNMP::Session(
        DestHost    => $host,
        Version     => $ver,
        Community   => $comm,
        Retries     => RETRIES,
        Timeout     => $timeout,
        UseSprintValue => 1,
    );
binmode STDOUT, ':raw:utf8'; # binmode STDOUT;

# Test connection
unless ($snmp) {
    print STDOUT _xml_output(
            count => {
                name    => $cmd.'::test',
                expires => [ $expires ],
                status  => [ 'ERROR' ],
                error   => { code    => 1, content => 
                        sprintf("Connect failed to %s (%s)",$host, $comm),
                    },
                value   => { type    => 'STR', content => '', },
            },
    );
    exit 0;
}
my $testval = '';
try {
    $testval = $snmp->get(TESTOBJ);
    my $testcode = $snmp->{ErrorNum} ? $snmp->{ErrorNum} : 0;
    my $testmsg  = $snmp->{ErrorStr} ? $snmp->{ErrorStr} : 'Undefined error';
    die sprintf("SNMP Error [%d]: %s. Can't get data of %s OID. Please check configuration of SNMPD on server %s (%s)",
            $testcode, $testmsg, TESTOBJ, $host, $comm
        ) unless $testval;
} catch {
    my $errtxt = $_;
    $errtxt =~ s/\s+at\s+.*$//;
    print STDOUT _xml_output(
            count => {
                name    => $cmd.'::test',
                expires => [ $expires ],
                status  => [ 'ERROR' ],
                error   => { code    => 2, content => cdata($errtxt), },
                value   => { type    => 'STR', content => '', },
            },
    );
    exit 0;
};
my $test = {
        name    => $cmd.'::test',
        expires => [ $expires ],
        status  => [ 'OK' ],
        error   => { code    => 0, content => '', },
        value   => { type    => 'STR', content => $testval, },
    };
    
if ($cmd eq 'get') {
    my $param = shift(@args) || 'SNMPv2-MIB::sysDescr.0'; # .1.3.6.1.2.1.1.1.0
    my $obj = '';
    my $val = '';
    
    if ($strictm) {
        $obj = [$param];
    } elsif ($param =~ /^\.?(\d+\.)*\d+$/) {
        if ($param =~ /\.0$/) {
            $obj = [$param];
        } else {
            $obj = new SNMP::Varbind([$param,0]);
        }
    } else {
        $param .= '.0' unless ($param =~ /\.0$/);
        $obj = [$param];
    }
    $val = $snmp->get($obj);
    $val = '' unless defined $val;
    #exception printf("SNMP Error [%d]: %s\n", $snmp->{ErrorNum}, $snmp->{ErrorStr}) if $snmp->{ErrorNum};
    if ($OPT{noxml}) {
        print $val;
    } else {
        print STDOUT _xml_output(
                count => [
                    {
                        name    => $param,
                        expires => [ $expires ],
                        status  => [ $snmp->{ErrorNum} ? 'ERROR' : 'OK' ],
                        error   => {
                                code    => $snmp->{ErrorNum} ? $snmp->{ErrorNum} : 0,
                                content => $snmp->{ErrorStr} ? cdata($snmp->{ErrorStr}) : '',
                            },
                        value   => {
                                type    => ($val =~ /^[+\-]\d+$/) ? 'DIG' : 'STR',
                                content => cdata($val),
                            },
                    },
                    $test,
                ],
            );
    }
} elsif ($cmd eq 'table') {
    my $table = shift(@args) || 'ifTable'; # TableName
    my $t = $snmp->gettable($table);
    #exception printf("SNMP Error [%d]: %s\n", $snmp->{ErrorNum}, $snmp->{ErrorStr}) if $snmp->{ErrorNum};
    if ($OPT{noxml}) {
        require Data::Dumper;
        print STDOUT Data::Dumper::Dumper($t);
    } else {
        print STDOUT _xml_output(
                count => [
                    {
                        name    => $table,
                        expires => [ $expires ],
                        status  => [ $snmp->{ErrorNum} ? 'ERROR' : 'OK' ],
                        error   => {
                                code    => $snmp->{ErrorNum} ? $snmp->{ErrorNum} : 0,
                                content => $snmp->{ErrorStr} ? cdata($snmp->{ErrorStr}) : '',
                            },
                        value   => {
                                type    => 'TAB',
                                record  =>  [$t],
                            },
                    },
                    $test,
                ],
            );
    }
} elsif ($cmd eq 'resources') {
    my $resource = lc(shift(@args) || ''); # Resource type
    my $data = {};
    
    #perl bin\alertgrid_snmp -dX -s mnssrv -c mnssrv -S table HOST-RESOURCES-MIB::hrStorageTable
    #UCD-SNMP-MIB::memAvailReal.0
    #HOST-RESOURCES-MIB::hrProcessorTable
    #HOST-RESOURCES-MIB::hrDeviceTable
    #HOST-RESOURCES-MIB::hrFSTable --   
    #UCD-SNMP-MIB::memTable
    my $t = $snmp->gettable("HOST-RESOURCES-MIB::hrStorageTable") || {};

    #    
    my $storageSize = 0;
    my $storageUsed = 0;
    my $storageFree = 0;
    my $storageUsedPercent = 0;
    my $storageFreePercent = 0;
    foreach (values %$t) {
        if ($_->{hrStorageType} && $_->{hrStorageType} eq '.1.3.6.1.2.1.25.2.1.4') {
            my $ss = _to_valid_int($_->{hrStorageSize} || 0);
            my $su = _to_valid_int($_->{hrStorageUsed} || 0);
            my $au = _units2bytes($_->{hrStorageAllocationUnits} || 1) || 1;
            $storageSize += ($ss * $au);
            $storageUsed += ($su * $au);
            $storageFree += (($ss - $su) * $au);
        }
    }
    $data->{hdd} = {
            Size        => $storageSize,
            Used        => $storageUsed,
            Free        => $storageFree,
            UsedPercent => _percent($storageUsed, $storageSize),
            FreePercent => _percent($storageFree, $storageSize),
        } if ((!$resource) || $resource eq 'hdd');
    
    #    
    my $memorySize = _units2bytes($snmp->get(["UCD-SNMP-MIB::memTotalReal.0"]) || 0);
    my $memoryFree = _units2bytes($snmp->get(["UCD-SNMP-MIB::memAvailReal.0"]) || 0);
    my $memoryUsed = $memorySize - $memoryFree;
    my $memoryFreePercent = _percent($memoryFree, $memorySize);
    my $memoryUsedPercent = _percent($memoryUsed, $memorySize);
    $data->{mem} = {
            Size        => $memorySize,
            Used        => $memoryUsed,
            Free        => $memoryFree,
            UsedPercent => $memoryUsedPercent,
            FreePercent => $memoryFreePercent,
        } if ((!$resource) || $resource eq 'mem');

    #   swap
    my $swapSize = _units2bytes($snmp->get(["UCD-SNMP-MIB::memTotalSwap.0"]) || 0);
    my $swapFree = _units2bytes($snmp->get(["UCD-SNMP-MIB::memAvailSwap.0"]) || 0);
    my $swapUsed = $swapSize - $swapFree;
    my $swapFreePercent = _percent($swapFree, $swapSize);
    my $swapUsedPercent = _percent($swapUsed, $swapSize);
    $data->{swp} = {
            Size        => $swapSize,
            Used        => $swapUsed,
            Free        => $swapFree,
            UsedPercent => $swapUsedPercent,
            FreePercent => $swapFreePercent,
        } if ((!$resource) || $resource eq 'swp');
        
    #   cpu
    my $cpuUser = _to_valid_int($snmp->get(["UCD-SNMP-MIB::ssCpuUser.0"]) || 0);
    my $cpuSystem = _to_valid_int($snmp->get(["UCD-SNMP-MIB::ssCpuSystem.0"]) || 0);
    my $cpuSize = 100;
    my $cpuUsed = $cpuUser + $cpuSystem;
    my $cpuFree = $cpuSize - $cpuUsed;
    my $cpuFreePercent = _percent($cpuFree, $cpuSize);
    my $cpuUsedPercent = _percent($cpuUsed, $cpuSize);
    $data->{cpu} = {
            Size        => $cpuSize,
            Used        => $cpuUsed,
            Free        => $cpuFree,
            UsedPercent => $cpuUsedPercent,
            FreePercent => $cpuFreePercent,
        } if ((!$resource) || $resource eq 'cpu');
    
    
    if ($OPT{noxml}) {
        require Data::Dumper;
        print STDOUT Data::Dumper::Dumper($data);
    } else {
        print STDOUT _xml_output(
                count => [
                    {
                        name    => 'resources',
                        expires => [ $expires ],
                        status  => [ $snmp->{ErrorNum} ? 'ERROR' : 'OK' ],
                        error   => {
                                code    => $snmp->{ErrorNum} ? $snmp->{ErrorNum} : 0,
                                content => $snmp->{ErrorStr} ? cdata($snmp->{ErrorStr}) : '',
                            },
                        value   => {
                                type    => 'TAB',
                                record  =>  [$data],
                            },
                    },
                    $test,
                ],
            );
    }
} elsif ($cmd eq 'traffic') {
    my @ifs = @args; # Indexes (numbers) of interfaces
    my $t = $snmp->gettable( 'ifTable' );
    @ifs = keys(%$t) unless @ifs;
    
    my %traffic = ();
    foreach my $ifk (keys %$t) {
        if (grep { _to_valid_int($_) == $ifk } @ifs) {
            my $ifd = $t->{$ifk};
            $traffic{$ifk} = {
                    In  => _to_valid_int($ifd->{ifInOctets}),
                    Out => _to_valid_int($ifd->{ifOutOctets}),
                };
        }
    }
    
    if ($OPT{noxml}) {
        require Data::Dumper;
        print STDOUT Data::Dumper::Dumper(\%traffic);
    } else {
        print STDOUT _xml_output(
                count => [
                    {
                        name    => 'traffic',
                        expires => [ $expires ],
                        status  => [ $snmp->{ErrorNum} ? 'ERROR' : 'OK' ],
                        error   => {
                                code    => $snmp->{ErrorNum} ? $snmp->{ErrorNum} : 0,
                                content => $snmp->{ErrorStr} ? cdata($snmp->{ErrorStr}) : '',
                            },
                        value   => {
                                type    => 'TAB',
                                record  =>  [\%traffic],
                            },
                    },
                    $test,
                ],
            );
    }
} else {
    pod2usage(-exitval => 1, -verbose => 0);
}

exit 0;

sub _void {
    #       (== undef)     -  
    my $v = shift;
    return '' unless defined $v;
    return $v;
}
sub _to_valid_int {
    #     0  99999999999     INT
    my $i = shift || 0;
    $i =~ s/[^0-9]//g;
    return 0 unless $i =~ /^[0-9]{1,}$/;
    return $i;
}
sub _xml_output {
    my %xmldata = @_;
    my $xmlout = XMLout(\%xmldata,
            RootName => 'response', 
            XMLDecl  => '<?xml version="1.0" encoding="utf-8"?>',
            NoEscape => 1,
        );
    return $xmlout
}
sub _units2bytes {
    my $in = shift;
    $in = '' unless defined $in;
    return 0 if $in eq '0';
    my %ut = %{(UNITS)};
    my $d = ($in =~ /(\d+)/) ? $1 : 1;
    my $w = ($in =~ /([a-z]+)/i) ? lc($1) : 'b';
    my $r = defined $ut{$w} ? $ut{$w} : 0;
    return $r * $d;
}
sub _percent {
    $a = shift || 0;
    $b = shift || 0;
    return '0.00' unless $b;
    return sprintf("%.2f", ($a/ $b) * 100);
}

1;
__END__

