#!/usr/bin/perl

#FILE: check_dns_host.pl
#SYNOPSIS: check A/PTR records for a host. Support for checking returnval vs. an array for matching valid response(s), threshhold for number of returned responses, and timeout values.
#RESPONSIBLE_PARTY: eli

use Net::DNS;
use Getopt::Long;
use Array::Compare;
use Pod::Usage;
use Time::HiRes qw{usleep ualarm gettimeofday tv_interval};


### Define ARGV options:
my ($host,@match,$server,$timeout,$qtimew,$qtimec,$retvalnum,$querytype);

GetOptions(
  "help"	=> \$help,
  "H|host=s"	=> \$host,
  "w|qtimew=i"	=> \$qtimew,
  "c|qtimec=i"	=> \$qtimec,
  "q|querytype=s" => \$querytype,
  "r|retvalnum=i" => \$retvalnum,
  "m|match=s"	=> \@match,
  "s|server=s"	=> \@server,
  "t|timeout=i"	=> \$timeout,
);

# set timeout and alarm for execution time allowed.
unless ($timeout) {$timeout=15};
alarm($timeout);
$SIG{ALRM} = sub {print "$0 timed out.\n"; exit 2;};

######################################################################################
### START code:
######################################################################################

# define program vars:
my ($header,$exitstate,@retval,$checkfailed);

# Initialize the DNS Resolver
my $res = Net::DNS::Resolver->new(
  nameservers => \@server,
  recurse     => 0,
  debug       => 0,
);

# define program flow:
#pod2usage unless ($host);;
&processargs;
&exit_eval(&query_dns);
&exit_eval(&query_check);

######################################################################################
### FUNC: ProcessArgs
sub processargs {

if ($man or $help) { 
  pod2usage(-verbose => 2,-noperldoc => 1,-exitval => 3);
  pod2usage(1);
  exit 1;
}

unless (defined ($host && $querytype && $qtimew && $qtimec)) {
  pod2usage("$0: Not enough arguments.\n");
}

}
######################################################################################
### FUNCTION: query_run
### Execute defined query:

sub query_dns {
  # Perform the lookup and time it
  $t0 = [gettimeofday];
  $query = $res->send("$host");
  $t1 = [gettimeofday];
  $elapsed = int (1000 * tv_interval ( $t0, $t1));

  # set result codes from query and lint-check it
  $query_returncode = ($query->{header}->{rcode});
  $query_count = ($query->{header}->{ancount});
  unless ($query_returncode eq "NOERROR") {
    $exitstate = "CRITICAL";
    $queryfailed = $query_returncode;
    return $queryfailed;
  } #/unless query_returncode is good

# Populate comparison array based on query type:
  # FORWARD lookup:
  if ($querytype eq "FORWARD") { # A queries:
      foreach my $rr ($query->answer) {
        next unless $rr->type eq "A";
        my $ip =  $rr->address;
            push (@retval, $ip);
      }
  } #/if ($querytype eq "FORWARD")

  # REVERSE lookup:
  if ($querytype eq "REVERSE") { # PTR queries:
    foreach my $rr ($query->answer) {
      next unless $rr->type eq "PTR";
      my $hostname =  $rr->ptrdname;
      push (@retval, $hostname);
    }
  } #/if ($querytype eq "REVERSE")
} #/sub query_dns

######################################################################################
### FUNCTION: query_check
### Check for failure conditions:
sub query_check {

# return/match array comparison:
if (defined (@match)) {
  @retval = sort(@retval);
  @match = sort(@match);
  $match_comp = Array::Compare->new(
    Sep => ' ',
    WhiteSpace => 0,
    DefFull => 0,
  );
  unless ($match_comp->compare(\@match, \@retval)) {
    my $match_mismatch = $match_comp->full_compare(\@match, \@retval); 
    $exitstate = "CRITICAL";
    $checkfailed = "MATCH";
    $checkfailed_err = qq{MATCH_ERRORS = "$match_mismatch", RETVAL:"@retval" != MATCH:"@match"};
    return $exitstate;
  }
}

# num query returns out of specified bounds:
if (defined ($retvalnum)) {
  unless ($retvalnum == $query_count) {
    $exitstate = "CRITICAL";
    $checkfailed = "RETVALNUM";
    $checkfailed_err = qq{RETVALNUM:"$retvalnum" != "QUERYCOUNT:$query_count"};
    return $exitstate;
  }
} 

# query crit/warn timeout threshhold exceeded:
if ($elapsed > $qtimec) {
  $exitstate = "CRITICAL";
  $checkfailed = "TIME_CRIT";
  $checkfailed_err = qq{ETIME:"$elapsed ms" >= TIME_CRIT:"$qtimec ms"};
  return $exitstate;
} elsif ($elapsed > $qtimew) {
  $exitstate = "WARNING";
  $checkfailed = "TIME_WARN";
  $checkfailed_err = qq{ETIME:"$elapsed ms" >= TIME_WARN:"$qtimew ms"};
  return $exitstate;
}

# Yay! all seems OK...
$exitstate = "OK";
return $exitstate;

} #/func: query_check

######################################################################################
### FUNCTION: exit_eval
sub exit_eval {

# Immediately exit on query failures:
if (defined ($queryfailed)) {
  print "DNS $exitstate! QUERYRETURNCODE=($query_returncode => $host) ANSWERS=($query_count) QUERY_RETURNVAL=(@retval) ELAPSED=(${elapsed} ms) | DurationMS=$elapsed", "\n";
  exit 2
}

# If query succeeded but we haven't checked our query yet:
unless (defined ($exitstate)) {
  return();
}

if ($exitstate eq "OK") {
  print "DNS $exitstate! QUERYRETURNCODE=($query_returncode => $host) ANSWERS=($query_count) QUERY_RETURNVAL=(@retval) ELAPSED=(${elapsed} ms) | DurationMS=$elapsed", "\n";
  exit 0;
} elsif ($exitstate eq "WARNING") {
  print "DNS $exitstate=($checkfailed) CHECK_ERR=($checkfailed_err) ANSWERS=($query_count) ELAPSED=(${elapsed} ms) | DurationMS=$elapsed", "\n";
  exit 1;
} elsif ($exitstate eq "CRITICAL") {
  print "DNS $exitstate=($checkfailed) CHECK_ERR=($checkfailed_err) ANSWERS=($query_count) ELAPSED=(${elapsed} ms) | DurationMS=$elapsed", "\n";
  exit 2;
} #/if
} #/func exit_eval


=head1 NAME

check_dns_host.pl - Perform A/PTR DNS query on supplied host, check return values/states.

=head1 SYNOPSIS

check_dns_host.pl -H [HOSTNAME] ... [REQ ARGS] ... [OPT ARGS]

=head1 OPTIONS

REQUIRED:

 -H, --host         The FQDN host to query for. NO search path supported.
 -Q, --querytype    The Query type: "FORWARD" = A | "REVERSE" = PTR
 -W, --wlevel       Query warning runtime treshhold (milliseconds)
 -C, --clevel       Query critical runtime treshhold (milliseconds)

EXTENDED CHECKS:

 -M, --match:       String* to match query RETURN value against, script will exit and report diff if not matching.
                    This is handled internally as an array comparison; this fully supports checking
                    a DNS-RR return, just provide multiple -M "$value" args on the commandline!

 -R, --retvalnum:   Script will exit (and return number of records present) if the number of records
                    returned by the query does NOT equal this number specified.

OPTIONAL:

 -S, --server       The DNS server IP to query against.
 -T, --timeout      The timeout threshhold in seconds until seppuku.
 --help, --man      This help.

=head1 DESCRIPTION

Supports extensive checks for validity, and correspondingly verbose exit messages reporting failure mode details.

Critical failure conditions:

DNS lookup fails (timeout, SRVFAIL, NXDOMAIN)
Match (if specified) does not reflect record(s) returned by query.
Number of records (if specified) does not reflect the number returned by query.
Critical query runtime threshhold exceeds specified value.

Warning failure only occurs if query runtime exceeds specified warn value.                                                                                

=head1 CAVEAT EMPTOR

This may just break your M$ DNS server... then again, that begat this in the first place :)

The array comparison for matching return values against "proper" values should probably return only the differences... then again maybe not.  Since this script is being executed an exceedingly large number of times, t may be worthwhile to move the (possibly) unused modules from loading if you don't use them (i.e. Array::Compare and Pod::Usage.  This is assuredly _not_ EPN safe.  Not using utils.pm since this gets called on non-nagios systems as well.

=head1 AUTHOR

Copyright (c) 2006 Eli Stair <estair@ilm.com>


