#!/usr/bin/perl -w
#
# check_yum/check_up2date - nagios plugin to check for outstanding updates via yum 
#   (or up2date if invoked as check_up2date)
#

use strict;
use File::Basename;
BEGIN {
  use lib dirname($0);
  use Nagios::Plugin;
}

my $VERSION = '0.2 [http://www.openfusion.com.au/labs/nagios/]';

my $USAGE = qq(usage: %s [-d default-level ] [-t timeout] [-v]
                 [-w warning-pkgs] [-W warning-repos]
                 [-c critical-pkgs] [-C critical-repos]
                 [-x exclude-pkgs] [-X exclude-repos]\n);
my ($warning_packages, $critical_packages, $exclude_packages);
my ($warning_repos, $critical_repos, $exclude_repos);
my $default = 'critical';
my @SPEC = (
  "warning|w=s"         => [ \$warning_packages,
    qq(-w, --warning=STRING
    Packages (comma-separated) to treat as warnings) ],
  "critical|c=s"        => [ \$critical_packages,
    qq(-c, --critical=STRING
    Packages (comma-separated) to treat as critical) ],
  "exclude|x=s"         => [ \$exclude_packages,
    qq(-x, --exclude=STRING
    Packages (comma-separated) to exclude/ignore) ],
  "warning-repo|W=s"    => [ \$warning_repos,
    qq(-W, --warning-repos=STRING
    Repositories (comma-separated) to treat as warnings) ],
  "critical-repo|C=s"   => [ \$critical_repos,
    qq(-C, --critical-repos=STRING
    Repositories (comma-separated) to treat as critical) ],
  "exclude-repo|X=s"    => [ \$exclude_repos,
    qq(-X, --exclude-repos=STRING
    Repositories (comma-separated) to exclude/ignore) ],
  "default|d=s"         => [ \$default,
    qq(-d, --default=STRING
    Default level: c | w | x | critical | warning | exclude (default: $default)) ],
);
my $extra = qq(
Package lists (-w, -c, -x etc.) take precedence over repository lists 
(-W, -C, -X etc.).

Tokens in package lists may include glob-style wildcards, as well as literals 
e.g. -x 'kernel*' -c 'httpd*,mod_*,rsync'. Repository lists do NOT support 
wildcards, however.

Note that $plugin probably needs to run as root to work correctly.
);

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

# Setup package data structures
my (%PKG, %PKG_REGEX);
sub pkg_setup
{
  my ($crit, $warn, $excl) = @_;
  %PKG = ( CRITICAL => {}, WARNING => {}, EXCLUDE => {} );
  $PKG{CRITICAL} = { map { $_ => 1 } split /,/, $crit } if $crit; 
  $PKG{WARNING}  = { map { $_ => 1 } split /,/, $warn } if $warn;
  $PKG{EXCLUDE}  = { map { $_ => 1 } split /,/, $excl } if $excl;

  # Separate out wildcard package designations
  %PKG_REGEX = ( CRITICAL => [], WARNING => [], EXCLUDE => [] );
  for my $level (qw(CRITICAL WARNING EXCLUDE)) {
    for my $pkg (keys %{$PKG{$level}}) {
      if ($pkg =~ m/\*/) {
        delete $PKG{$level}->{$pkg};
        # Turn glob-style package designation (e.g. kernel*) into regex
        $pkg =~ s/\*/.*/g;
        $pkg =~ s/^/^/;
        $pkg =~ s/$/\$/;
        print "Adding regex '$pkg' to $level package wildcards\n" if $verbose >= 2;
        push @{$PKG_REGEX{$level}}, $pkg;
      }
    }
  }
}

# Match the given repo against the entries in the repo hash
sub pkg_match
{
  my $pkg = shift;
  return unless $pkg;

  # Check literal package names
  for my $level (qw(CRITICAL WARNING EXCLUDE)) {
    return $level if exists $PKG{$level}->{$pkg};
  }
 
  # Check package regexes
  for my $level (qw(CRITICAL WARNING EXCLUDE)) {
    for my $re (@{$PKG_REGEX{$level}}) {
      return $level if $pkg =~ m/$re/i;
    }
  }
}

# Setup repository data structures
my %REPO;
sub repo_setup
{
  my ($crit, $warn, $excl) = @_;
  %REPO = ( CRITICAL => {}, WARNING => {}, EXCLUDE => {} );
  $REPO{CRITICAL} = { map { $_ => 1 } split /,/, $crit } if $crit;
  $REPO{WARNING}  = { map { $_ => 1 } split /,/, $warn } if $warn;
  $REPO{EXCLUDE}  = { map { $_ => 1 } split /,/, $excl } if $excl;
}

# Match the given repo against the entries in the repo hash
sub repo_match
{
  my $repo = shift;
  return unless $repo;
  for my $level (qw(CRITICAL WARNING EXCLUDE)) {
    return $level if exists $REPO{$level}->{$repo};
  }
}

# ------------------------------------------------------------------------
# Main

my $MODE = $plugin;
$MODE =~ s/^check_//;
my $EXE = -x "/usr/sbin/$MODE" ? "/usr/sbin/$MODE" : "/usr/bin/$MODE";
my %EXE_ARGS = (
  yum => 'check-update',
  up2date => '--nox --list',
);

&exit("UNKNOWN", "invalid mode '$MODE'") unless exists $EXE_ARGS{$MODE};
&exit("UNKNOWN", "cannot find $EXE") unless -f $EXE && -x $EXE;

parse_args(
  version => $VERSION,
  usage => $USAGE,
  spec => \@SPEC,
  help_extra => $extra,
);

&exit("UNKNOWN", "invalid default '$default'") 
  if $default && ! grep /^$default$/i, qw(c w x critical warning exclude);
$default = uc substr($default,0,1);

# Do the yum/up2date check
alarm($timeout);
my $cmd = "$EXE $EXE_ARGS{$MODE}";
print "cmd: $cmd\n" if $verbose >= 2;
my $out = `$cmd`;
my $rc = $? >> 8;
alarm(0);

# Up to date
if ($MODE eq 'yum' && $rc == 0) {
  &exit("OK", "all packages up to date");
}

# Unknown error
elsif ($MODE eq 'yum' && $rc != 100) {
  &exit("UNKNOWN", "$MODE check failed ($rc):\n$out");
} 

# Updates found, or up2date mode (rc's are bogus and inconsistent)
else {
  my $results;
  # Get set of updates
  my $packages = $out;
  $packages =~ s/^.*?-----\n//s;
  $packages =~ s/\n\n.*$//s;
  $packages =~ s/^\n.*$//s;
  print "packages:\n$packages\n" if $verbose >= 2;
  my @updates = split /\s*\n/, $packages;
  unless (@updates) {
    $results = "all packages up to date\n";
    $results .= $packages if $verbose;
    &exit("OK", $results);
  }

  # Iterate over update packages
  my @warning = ();
  my @critical = ();
  pkg_setup($critical_packages, $warning_packages, $exclude_packages);
  repo_setup($critical_repos, $warning_repos, $exclude_repos);
  for my $update (@updates) {
    my ($pkg,$arch,$ver,$repo) = split /\s+/, $update;

    # Categorise this package
    my $level = '';
    if ($level = pkg_match($pkg)) {
      if ($level eq 'CRITICAL') {
        push @critical, $pkg;
      } elsif ($level eq 'WARNING') {
        push @warning, $pkg;
      } else {
        next;
      }
    } elsif ($level = repo_match($repo)) {
      if ($level eq 'CRITICAL') {
        push @critical, $pkg;
      } elsif ($level eq 'WARNING') {
        push @warning, $pkg;
      } else {
        next;
      }
    }
    # Fallback to default action
    elsif ($default eq 'C') {
      push @critical, $pkg;
    } elsif ($default eq 'W') {
      push @warning, $pkg;
    }
  }

  # Return OK unless we have critical or warning packages
  unless (@critical || @warning) {
    $results = "all packages up to date\n";
    $results .= sprintf("(%d updates found, but all excluded)\n",
      scalar(@updates)) if $verbose;
    $results .= $packages if $verbose;
    &exit("OK", $results);
  }

  # Output results
  $results = sprintf "updates found: %d critical, %d warning\n", 
    scalar(@critical), scalar(@warning);
  $results .= $packages if $verbose;

  &exit(@critical ? "CRITICAL" : "WARNING", $results);
}


# arch-tag: 6b5e3849-a597-4185-b926-912f13a27e88
