#!/usr/bin/perl -w
#
# check_file - plugin to check various constraints on a given file
#   e.g. mtime, size, contents
# Returns CRITICAL if any of the asserted constraints fail.
# Intended for checking cron job output, for instance.
#

use strict;
use File::Basename;
use Nagios::Plugin::Getopt;
use Nagios::Plugin 0.1301;

my $ng = Nagios::Plugin::Getopt->new(
  usage => q(Usage: %s -f /path/to/file [-m <mtime>] [-s <size>] 
         [-r <regex>] [-e <error-regex>] [-i] [-t timeout] [-v]),
  version => '0.04',
  url => 'http://www.openfusion.com.au/labs/nagios/',
  blurb => q(This plugin checks various constraints on a given file.),
  extra => qq(MTIME and SIZE may be specified using common units e.g. 30s, 30m, 30h, 30d
for MTIME, and 30B, 30K, 30KB, 30MB, 30G for SIZE.

MTIME and SIZE parameters may also have a prefixing modifier sign indicating 
the range specified by the constraint, as follows: '-' (minus) indicates the
relevant value must be less than the given constraint; '=' (equals) indicates
the relevant value must exactly equal the given constraint; and '+' (plus)
indicates the given value must be greater than or equal to the given 
constraint.),
);

$ng->arg(
  spec => "file|f=s@",
  help => q(-f, --file=PATH
   File to check (may be repeated)),
  required => 1);
$ng->arg("mtime|m=s",
  q(-m, --mtime=MTIME
   Modification time of file (default: maximum age in seconds)));
$ng->arg("size|s=s",
  q(-s, --size=SIZE
   Size of file (default: minimum size in bytes)));
$ng->arg("regex|r=s@",
  q(-r, --regex=REGEX
   Regex that should be found in file (may be repeated)));
$ng->arg("error-regex|e=s@",
  q(-e, --error-regex=REGEX
   Regex that should NOT be found in file (may be repeated)));
$ng->arg("ignore-case|i",
  q(-i, --ignore-case
   Ignore case in regex tests));

$ng->getopts;

# ----------------------------------------------------------------------------
# Subroutines

my %TMULT = ();
$TMULT{$_} = 1         foreach qw(s sec secs);
$TMULT{$_} = 60        foreach qw(m min mins);
$TMULT{$_} = 3600      foreach qw(h hr hrs hour hours);
$TMULT{$_} = 3600 * 24 foreach qw(d day days);

# Convert time period designations to seconds
#   e.g. 33s, 33sec, 33secs, 33m, 33min, 33mins, 33h, 33d, etc.
sub period_to_sec
{
  my $t = shift;
  my $sec = 0;
  if ($t =~ m/^\s*([-+=]?)\s*(\d+(\.\d+)?)\s*([a-zA-Z]+)/ && $TMULT{lc $4}) {
    $sec = ($1 || '') . ($2 * $TMULT{lc $4});
  }
  elsif ($t =~ m/^\s*([-+=]?)\s*(\d+(\.\d+)?)/) {
    $sec = ($1 || '') . $2;
  }
  return $sec;
}

# These should maybe be powers of 2, but round numbers are easier to eyeball
my %SMULT = ();
$SMULT{$_} = 1                  foreach qw(b bytes);
$SMULT{$_} = 1000               foreach qw(k kb);
$SMULT{$_} = 1000 * 1000        foreach qw(m mb);
$SMULT{$_} = 1000 * 1000 * 1000 foreach qw(g gb);

# Convert size units designations to bytes
sub size_to_bytes
{
  my $s = shift;
  my $bytes = 0;
  if ($s =~ m/^\s*([-+=]?)\s*(\d+(\.\d+)?)\s*([a-zA-Z]+)/ && $SMULT{lc $4}) {
    $bytes = ($1 || '') . ($2 * $SMULT{lc $4});
  }
  elsif ($s =~ m/^\s*([-+=]?)\s*(\d+(\.\d+)?)/) {
    $bytes = ($1 || '') . $2;
  }
  return $bytes;
}

# Decorate $value with the default $signum unless already decorated
sub decorate
{
  my ($value, $signum) = @_;
  return $value if ! $value || $value =~ m/^\s*[-+=]/;
  die "bad signum '$signum'" unless $signum =~ m/^[-+=]$/;
  $value =~ s/^\s*/$signum/;
  return $value;
}

# Return true if $value does NOT match signum-decorated $parameter (+ is >=, - is <, = is ==)
sub violate
{
  my ($value, $param) = @_;
  if ($param =~ m/^=(.*)/) {
    return $value != $1;
  }
  elsif ($param =~ m/^-(.*)/) {
    return $value >= $1;
  }
  elsif ($param =~ m/^\+(.*)/) {
    return $value < $1;
  }
}

# ----------------------------------------------------------------------------

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

# At least one of $size, $mtime, @regex, or @error must be set
$np->nagios_die("nothing to check - no mtime, size, regex, or error regex checks found")
    unless $ng->mtime || $ng->size || @{$ng->regex} || @{$ng->error};

my $mtime = $ng->mtime;
my $size = $ng->size;
if ($mtime) {
  my $mtime_sec = period_to_sec($mtime);
  $np->nagios_die("failed to parse mtime '$mtime'") unless $mtime_sec;
  $mtime_sec = decorate($mtime_sec, '-');
  print STDERR "+ $mtime converted to $mtime_sec\n" 
    if $ng->verbose && $mtime ne $mtime_sec;
  $mtime = $mtime_sec;
}
if ($size) {
  my $size_bytes = size_to_bytes($size);
  $np->nagios_die("failed to parse size '$size'") unless $size_bytes;
  $size_bytes = decorate($size_bytes, '+');
  print STDERR "+ $size converted to $size_bytes\n" 
    if $ng->verbose && $size ne $size_bytes;
  $size = $size_bytes;
}

my (@crit, @ok);
for my $file (@{$ng->file}) {
  # Check existence
  if (! -e $file) {
    push @crit, "$file: does not exist";
    next;
  }
  my (@violations, @attr);
  # Check constraints
  if ($mtime || $size) {
    my ($s, $m) = (stat $file)[7,9];
    if ($mtime) {
      my $age = time() - $m;
      push @violations, "bad mtime (${age}s)" if violate($age, $mtime);
      push @attr, "mtime ${age}s";
    }
    if ($size) {
      push @violations, "bad size (${s}B)" if violate($s, $size);
      push @attr, "size ${s}b";
    }
  }
  if (($ng->regex && @{$ng->regex}) || 
      ($ng->get('error_regex') && @{$ng->get('error_regex')})) {
    # Slurp file contents
    open FILE, "<$file" or 
      $np->nagios_die("open on '$file' failed: $!");
    my $content = '';
    {
      local $/ = undef;
      $content = <FILE>;
    }
    close FILE;
     
    # Check regexes
    my $ic = $ng->get('ignore-case') ? '(?i)' : '';
    for my $regex (@{$ng->regex}) {
      if ($content =~ m/$ic$regex/) {
        push @attr, "regex /$regex/ matches";
     }
     else {
        push @violations, "no match for regex /$regex/";
      }
    }
    for my $error (@{$ng->error}) {
      if ($content =~ m/$ic$error/) {
        push @violations, "error regex /$error/ matches";
      }
      else {
        push @attr, "no match for error regex /$error/";
      }
    }
  }
  if (@violations) {
    push @crit, "$file: " . join(', ', @violations);
  }
  else {
    my $ok = $file;
    $ok .= sprintf " (%s)", join(', ', @attr) if @attr;
    push @ok, $ok;
  }
}

my $message = join ' :: ', @crit;
$message .= '  OK: ' . join(', ', @ok) if @ok;
my $code = $np->check_messages(critical => \@crit);
$np->nagios_exit($code, $message);


# arch-tag: 59479678-04b6-4a81-b99d-950f18f7c4ae
# vim:ft=perl
