#!/usr/bin/perl

# Copyright (c) 2016 Jason Filley <jason@snakelegs.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
# ------
#
# Perl check for DHCP pools via SNMP bulkwalk.  Specifically written to monitor Windows 2012R2 DHCP.
# -- Fast. Checks ~1000 scopes in 1.5 seconds.
# -- Lightweight.  Perl-only.  Only two SNMP queries.
# -- Defaults to including all scopes, which is most meaningful for ops and NOC.
#    -- Easy to specify inclusions only. For example, can specify a specific location's scopes.
#    -- Allow excluding scopes, such as small test scopes you'd never want alerted on.
#    -- "Include" trumps the default "all" or any specified "exclude" scopes.
# -- Allows defining a backup DHCP server, when primary is under maintenance. Useful when monitoring
#    specific scopes.
# -- Includes perfdata, disabled by default.
# -- Output sorted by criticality -> scope.  Any Critical, then Warning, are at the top of the output.

use SNMP;
#use warnings; #complains about sorting uninitialized variables, but that's exactly what I want to sort out
use strict;
use Nagios::Plugin;
use Nagios::Plugin::Getopt;

my $np = Nagios::Plugin->new;

my $ng = Nagios::Plugin::Getopt->new(
    'usage'   => 'Usage: %s -H <host> -C <SNMP community> -w <warning> -c <critical> (see --help for more options)',
    'version' => '0.0.1',
    'license' => 'Copyright (c) 2016 Jason Filley <jason@snakelegs.org>. '
      . 'Permission to use, copy, modify, and distribute this software for any '
      . 'purpose with or without fee is hereby granted, provided that the above '
      . 'copyright notice and this permission notice appear in all copies. '
      . 'THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES '
      . 'WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF '
      . 'MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR '
      . 'ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES '
      . 'WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN '
      . 'ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF '
      . 'OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.',
);

$ng->arg(
    spec     => 'host|H=s',
    help     => "hostname or IP address",
    required => 1,
);

$ng->arg(
    spec     => 'backup|b=s',
    help     => "backup/failover hostname",
    required => 0,
);

$ng->arg(
    spec     => 'port|p=i',
    help     => 'SNMP port (default: 161)',
    required => 0,
    default  => 161,
);

$ng->arg(
    spec     => 'community|C=s',
    help     => 'SNMP community name',
    required => 1,
);

$ng->arg(
    spec     => 'warning|w=i',
    help     => 'Minimum percent free leases for status WARNING',
    required => 1,
);

$ng->arg(
    spec     => 'critical|c=i',
    help     => 'Minimum percent free leases for status CRITICAL',
    required => 1,
);

$ng->arg(
    spec     => 'perfdata|P=i',
    help     => 'Include perfdata (default: 0 [no] -- set to 1 for [yes])',
    required => 0,
    default  => 0,
);

$ng->arg(
    spec     => 'include|i=s',
    help     => 'include only comma-delimited list of scope IDs',
    required => 0,
);

$ng->arg(
    spec     => 'exclude|x=s',
    help     => 'exclude comma-delimited list of scope IDs',
    required => 0,
);

$ng->getopts;
my $hostname    = $ng->host;
my $backup      = $ng->backup;
my $port        = $ng->port;
my $community   = $ng->community;
my $WARN        = $ng->warning;
my $CRIT        = $ng->critical;
my $includeperf = $ng->perfdata;
my $include     = $ng->include;
my $exclude     = $ng->exclude;

unless ( ( 0 < $CRIT ) && ( $CRIT < $WARN ) && ( $WARN < 100 ) ) {
    $np->nagios_exit( UNKNOWN, "bad values: WARN=$WARN CRIT=$CRIT" );
}

my @includes = ();
if ( defined $include ) {
    @includes = split ",", $include;
}

my @excludes = ();
if ( defined $exclude ) {
    @excludes = split ",", $exclude;
}

my $usedOID = '.1.3.6.1.4.1.311.1.3.2.1.1.2';
my $freeOID = '.1.3.6.1.4.1.311.1.3.2.1.1.3';
my %scopes;
my $return = 0;    #default to 0=OK.  Keep bumping up if greater errorlevel on scopes
my @resp;

# Try primary DHCP server
eval { @resp = snmpquery($hostname); };

# If error, try backup DHCP server, if defined
if ( $@ || scalar(@resp) == 1 ) {
    if ( defined $backup ) {
        eval { @resp = snmpquery($backup); };

        if ($@) {
            $np->nagios_exit( UNKNOWN, "Failed to retrieve results from both primary ($hostname) and backup ($backup) servers");
        }

    } else {
        $np->nagios_exit( UNKNOWN, "Failed to retrieve results from primary ($hostname), and no backup server defined");
    }
}

sub snmpquery {
    my $sess;

    eval {
        my $snmphost = shift;

        $sess = new SNMP::Session(
            'DestHost'       => $snmphost,
            'Community'      => $community,
            'RemotePort'     => $port,
            'Timeout'        => 300000,
            'Retries'        => 3,
            'Version'        => '2c',
            'UseLongNames'   => 0,
            'UseNumeric'     => 1,
            'UseEnums'       => 0,
            'UseSprintValue' => 0
        );

        my $vars = new SNMP::VarList( [$freeOID], [$usedOID] );

        @resp = $sess->bulkwalk( 0, 1, $vars );
    };

    if ($@) {
        die "Cannot do bulkwalk: $sess->{ErrorStr}";
    } else {
        return @resp;
    }
}

# populate 'free' and 'used'
for my $vbarr (@resp) {
    for my $v (@$vbarr) {
        my $scope = substr( $v->name, ( length($freeOID) + 1 ) );
        my $curoid = substr( $v->name, 0, length($freeOID) );
        if ( $curoid eq $freeOID ) {
            $scopes{$scope}{"free"} = $v->val;
        } elsif ( $curoid eq $usedOID ) {
            $scopes{$scope}{"used"} = $v->val;
        } else {
            $np->nagios_exit( UNKNOWN, "SNMP value returned that wasn't free or used\n" );
        }
    }
    print "\n";
}

my @perfoutput = ();
my $longoutput;

foreach my $scope ( keys %scopes ) {
    if (( !(defined $include) && ((defined $exclude) && !(grep {$_ eq $scope} @excludes)))
        || ((defined $include) && ( grep {$_ eq $scope} @includes ))
        || ( !(defined $include) && !(defined $exclude))) {
        $scopes{$scope}{"subnet"} = ($scope);
        $scopes{$scope}{"max"} = ($scopes{$scope}->{'free'}) + ($scopes{$scope}->{'used'});

        my $output;

        if ($scopes{$scope}{"max"} != 0 ) {
            $scopes{$scope}{"pctfree"} = int( ( $scopes{$scope}->{'free'} * 100 ) / $scopes{$scope}->{'max'});
            $scopes{$scope}{"pctused"} = 100 - $scopes{$scope}{"pctfree"};

            if ( $scopes{$scope}{"pctfree"} < $CRIT ) {
                $return                       = 2;
                $scopes{$scope}{"errorlevel"} = 2;
                $output                       = "Critical:";
            } elsif ( $scopes{$scope}{"pctfree"} < $WARN ) {
                if ( $return == 0 ) {
                    $return = 1;
                }
                $scopes{$scope}{"errorlevel"} = 1;
                $output = "Warning:";
            } else {
                $scopes{$scope}{"errorlevel"} = 0;
                $output = "OK:";
            }

            $output = $output . "$scope - $scopes{$scope}->{'pctfree'}% free - $scopes{$scope}->{'used'}/$scopes{$scope}->{'max'}";
            push @perfoutput, "'$scope'=$scopes{$scope}->{'pctused'}%;" . ( 100 - $WARN ) . ";" . ( 100 - $CRIT ) . ";0;100";

            $scopes{$scope}{"output"} = $output;

        }
        else {
            $scopes{$scope}{"errorlevel"} = 0;
            $scopes{$scope}{"output"} = "OK:$scope - scope has 0 maximum capacity. All leases reserved? Inactive?";
        }
    }
}

foreach my $scope ( sort { $scopes{$b}->{"errorlevel"} <=> $scopes{$a}->{"errorlevel"} || $scopes{$a}->{"subnet"} <=> $scopes{$b}->{"subnet"} } keys %scopes) {
    if ( exists( $scopes{$scope}{"errorlevel"} ) ) {
        $longoutput = $longoutput . "$scopes{$scope}->{'output'}\n";
    }
}

if ( $includeperf == 1 ) {
    $longoutput = $longoutput . "|" . join( ' ', @perfoutput );
}

if ( $return == 0 ) {
    $np->nagios_exit( OK, "All scopes fine" . "\n$longoutput" );
}
elsif ( $return == 1 ) {
    $np->nagios_exit( WARNING, "One or more scopes is nearing capacity" . "\n$longoutput" );
}
elsif ( $return == 2 ) {
    $np->nagios_exit( CRITICAL, "One or more scopes is nearing capacity" . "\n$longoutput" );
}
else {
    $np->nagios_exit( UNKNOWN, "problem running script" );
}


