#!/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"); }