#!/usr/bin/perl # Copyright (c) 2016 Jason Filley # # 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 -C -w -c (see --help for more options)', 'version' => '0.0.1', 'license' => 'Copyright (c) 2016 Jason Filley . ' . '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" ); }