#!/usr/bin/perl

eval 'exec /usr/bin/perl  -S $0 ${1+"$@"}'
    if 0; # not running under some shell

# check_updates is a Nagios plugin to check if RedHat or Fedora system
# is up-to-date
#
# See  the INSTALL file for installation instructions
#
# Copyright (c) 2007-2010, Matteo Corti
#
# This module is free software; you can redistribute it and/or modify it
# under the terms of GNU general public license (gpl) version 3,
# or (at your option) any later version.
# See the COPYING file for details.
#
# RCS information
# enable substitution with:
#   $ svn propset svn:keywords "Id Revision HeadURL Source Date"
#
#   $Id: check_updates 1207 2010-11-18 10:17:33Z corti $
#   $Revision: 1207 $
#   $HeadURL: https://svn.id.ethz.ch/nagios_plugins/check_updates/check_updates $
#   $Date: 2010-11-18 11:17:33 +0100 (Thu, 18 Nov 2010) $

use strict;
use warnings;
use Carp;

our $VERSION = '1.4.11';

use English qw(-no_match_vars);
use Nagios::Plugin::Getopt;
use Nagios::Plugin::Threshold;
use Nagios::Plugin;
use POSIX qw(uname);
use Sort::Versions;

use Readonly;

Readonly our $EXIT_UNKNOWN                 => 3;
Readonly our $YUM_RETURN_UPDATES_AVAILABLE => 100;

# IMPORTANT: Nagios plugins could be executed using embedded perl in this case
#            the main routine would be executed as a subroutine and all the
#            declared subroutines would therefore be inner subroutines
#            This will cause all the global lexical variables not to stay shared
#            in the subroutines!
#
# All variables are therefore declared as package variables...
#
use vars qw(
  $bootcheck
  $exit_message
  $help
  $options
  $plugin
  $threshold
  $security_plugin
  $wrong_kernel
  $status
  @status_lines
);

$status = OK;

##############################################################################
# subroutines

##############################################################################
# Usage     : whoami()
# Purpose   : retrieve the user runnging the process
# Returns   : username
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub whoami {
    my $output;
    my $pid = open $output, q{-|}, 'whoami'
      or
      $plugin->nagios_exit( UNKNOWN, "Cannot determine the user: $OS_ERROR" );
    while (<$output>) {
        chomp;
        return $_;
    }
    close $output;

    $plugin->nagios_exit( UNKNOWN, 'Cannot determine the user' );
    return;
}

##############################################################################
# Usage     : verbose("some message string", $optional_verbosity_level);
# Purpose   : write a message if the verbosity level is high enough
# Returns   : n/a
# Arguments : message : message string
#             level   : options verbosity level
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub verbose {

    # arguments
    my $message = shift;
    my $level   = shift;

    if ( !defined $message ) {
        $plugin->nagios_exit( UNKNOWN,
            q{Internal error: not enough parameters for 'verbose'} );
    }

    if ( !defined $level ) {
        $level = 0;
    }

    if ( $level < $options->verbose ) {
        print $message;
    }

    return;

}

sub clean_kernel_version {

    my $kernel = shift;

    # remove PAE
    
    $kernel =~ s/\.[\w]*PAE//imxs;
    $kernel =~ s/PAE-//imxs;

    # remove Xen
    $kernel =~ s/xen-//imxs;
    
    # remove architecture
    
    $kernel =~ s/\.(i[3-6]86|ppc|x86_64)$//mxs;

    # remove smp

    $kernel =~ s/smp$//mxs;
    $kernel =~ s/smp-//imxs;

    # remove RH, Fedora and CentOSflavours

    $kernel =~ s/\.el\d+.*//imxs;
    $kernel =~ s/.fc\d+.*//imxs;

    return $kernel;

}

##############################################################################
# Usage     : check_running_kernel( );
# Purpose   : checks if the loaded kernel is the latest available
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub check_running_kernel {

    if ( !$bootcheck ) {
        return;
    }

    my $package = 'kernel';

    ################################################################################
    # Running

    my ( $sysname, $nodename, $release, $version, $machine ) = POSIX::uname();

    $release = clean_kernel_version($release);

    verbose "running a Linux kernel: $release\n";

    ################################################################################
    # Installed

    my @versions;

    for my $rpm ( ( "$package", "$package-smp", "$package-PAE", "$package-xen" ) ) {

        my $output;

        my $pid = open $output, q{-|}, "rpm -q $rpm"
          or $plugin->nagios_exit( UNKNOWN,
            "Cannot list installed kernels: $OS_ERROR" );

        # there could be multiple versions of the same package installed
        my @rpm_versions;
        while (<$output>) {
            chomp $_;
            push @rpm_versions, $_;
        }

        if ( !( close $output ) && ( $OS_ERROR != 0 ) ) {

            # close to a piped open return false if the command with non-zero
            # status. In this case $! is set to 0
            $plugin->nagios_exit( UNKNOWN,
                "Error while closing pipe to rpm: $OS_ERROR\n" );

        }

        if ( $CHILD_ERROR == 0 ) {

            # rpm exits with 0 only it the RPM exists

            push @versions, @rpm_versions;
        }

    }

    for (@versions) {

        # strip package name
        $_ =~ s/^$package-//mxs;
        $_ = clean_kernel_version($_);

    }

    @versions = sort versioncmp @versions;

    my $installed = $versions[-1];

    verbose "kernel: running = $release, installed = $installed\n";

    if ( $installed ne $release ) {

        my $error =
"your machine is running kernel $release but a newer version ($installed) is installed: you should reboot";

        if ($exit_message) {
            $exit_message .= $error;
        }
        else {
            $exit_message = $error;
        }

        $wrong_kernel = 1;

    }

    return;

}

##############################################################################
# Usage     : run_yum();
# Purpose   : runs yum check-updates with the supplied command line argumens
# Returns   : the list of updates
# Arguments : string with the command line arguments
# Throws    : n/a
# Comments  : sets $security_plugin to true if the plugin is enabled
# See also  : n/a
sub run_yum {

    my $arguments = shift;

    my $blank_line;
    my $OUTPUT_HANDLER;
    my @updates;

    my $command = "yum $arguments check-update";

    my $pid = open $OUTPUT_HANDLER, q{-|}, $command
      or $plugin->nagios_exit( UNKNOWN, "Cannot list updates: $OS_ERROR" );
    
    while (<$OUTPUT_HANDLER>) {

        my $line = $_;

        verbose $line, 1;

        chomp $line;

        if ( !$line ) {
            $blank_line = 1;
            next;
        }

        if ( $line =~ m{^Loaded\ plugins:.*\ security.*}mxs ) {

            if ( !$security_plugin ) {

             # we do not want the same output if we are running the second check
                verbose "Security plugin installed\n";
            }
            $security_plugin = 1;

        }
        
        if ($blank_line) {

            # some lines are wrapped and result and the second part
            # is erroneously interpreted as a new update
            if ($line =~ m/^\ / ) {
                next;
            }

            $line =~ s{\ .*}{}mxs;

            push @updates, $line;
        }

    }

    close $OUTPUT_HANDLER;

    if ( ( $CHILD_ERROR >> 8 ) == $YUM_RETURN_UPDATES_AVAILABLE ) {
        if ($security_plugin) {
            verbose "Security updates available\n";
        }
        else {
            verbose "Updates available\n";
        }
    }

    return @updates;

}

##############################################################################
# Usage     : check_yum();
# Purpose   : checks a yum based system for updates
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub check_yum {

    my $message;
    my @outdated = run_yum(q{});
    my @security_updates;

    $plugin->add_perfdata(
        label     => 'total_updates',
        value     => scalar @outdated,
        uom       => q{},
        threshold => $threshold,
    );

    if ( @outdated > 0 ) {

        if ( !$security_plugin ) {

            # every update is critical since it could be a security problem

            $status = CRITICAL;
            verbose "yum reports a non-up-to-date system\n";

            $message = ( scalar @outdated ) . ' update';
            if ( @outdated > 1 ) {
                $message = $message . q{s};
            }
            $message = $message . ' available';

            @status_lines = @outdated;

        }
        else {

            $status = WARNING;

            @security_updates = run_yum('--security');

            if ( @security_updates > 0 ) {

                $status = CRITICAL;

                $message = ( scalar @security_updates ) . ' security update';
                if ( @security_updates > 1 ) {
                    $message = $message . q{s};
                }

                # compute the array difference
                my %count;

                for my $element (@security_updates) {
                    push @status_lines, "$element (security)";
                    $count{$element}++;
                }

                for my $element (@outdated) {
                    $count{$element}++;
                }

                my @difference;
                for my $element (@outdated) {
                    if ( $count{$element} == 1 ) {
                        push @difference, $element;
                    }
                }

                @outdated = @difference;

            }

            if ( @outdated > 0 ) {

                if ($message) {
                    $message .= ' and ';
                }

                $message .= ( scalar @outdated ) . ' non-critical update';
                if ( @outdated > 1 ) {
                    $message = $message . q{s};
                }

                push @status_lines, @outdated;

            }

            if ($message) {
                $message .= ' available';
            }

        }

    }
    else {
        $status  = OK;
        $message = 'no updates available';
    }

    $plugin->add_perfdata(
        label     => 'security_updates',
        value     => scalar @security_updates,
        uom       => q{},
        threshold => $threshold,
    );

    if ($exit_message) {
        $exit_message .= q{, } . $message;
    }
    else {
        $exit_message = $message;
    }

    return;

}

##############################################################################
# Usage     : check_up2date();
# Purpose   : checks an up2date based system for updates
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub check_up2date {

    # parsing the output of up2date -l

    if ( whoami() ne 'root' ) {
        $plugin->nagios_exit( CRITICAL,
            q{must be root to execute 'up2date -l': use sudo} );
    }

    my $output;

    my $command =
q{/usr/sbin/up2date -lf | /bin/grep -A 64 -- '----------------------------------------------------------' | /bin/grep '[[:alpha:]]'};

    my $pid = open $output, q{-|}, $command
      or $plugin->nagios_exit( UNKNOWN, "Cannot list updates: $OS_ERROR" );

    while (<$output>) {
        chomp;
        my $line = $_;
        $line =~ s/\ .*//mxs;
        push @status_lines, $line;
    }

    close $output;

    my $message;

    if ( @status_lines > 0 ) {

        $message = ( scalar @status_lines ) . ' update';

        $status = CRITICAL;

        if ( @status_lines > 1 ) {
            $message = $message . q{s};
        }
        $message = $message . ' available';
        $plugin->add_perfdata(
            label     => 'updates',
            value     => scalar @status_lines,
            uom       => q{},
            threshold => $threshold,
        );

    }
    else {

        $message = 'no updates available';

        $plugin->add_perfdata(
            label     => 'updates',
            value     => 0,
            uom       => q{},
            threshold => $threshold,
        );

    }

    if ($exit_message) {
        $exit_message .= " $message";
    }
    else {
        $exit_message = $message;
    }

    return;

}

##############################################################################
# main
#

################
# initialization
$help         = q{};
$bootcheck    = 1;
$wrong_kernel = 0;
$plugin       = Nagios::Plugin->new( shortname => 'CHECK_UPDATES' );

########################
# Command line arguments

$options = Nagios::Plugin::Getopt->new(
    usage   => 'Usage: %s [OPTIONS]',
    version => $VERSION,
    url     => 'https://trac.id.ethz.ch/projects/nagios_plugins',
    blurb   => 'Checks if RedHat or Fedora system is up-to-date',
);

$options->arg(
    spec => 'boot-check',
    help => 'Check if the machine was booted with the newest kernel (default)',
);

$options->arg(
    spec => 'no-boot-check',
    help => 'do not complain if the machine was booted with an old kernel',
);

$options->arg(
    spec => 'warning|w=i',
    help =>
      'Exit with WARNING status if more than INTEGER updates are available',
    default => 0
);

$options->arg(
    spec => 'critical|c=i',
    help =>
      'Exit with CRITICAL status if more than INTEGER updates are available',
    default => 0
);

$options->getopts();

$threshold = Nagios::Plugin::Threshold->set_thresholds(
    warning  => $options->get('warning'),
    critical => $options->get('critical'),
);

# check bootcheck consistency
if ( $options->get('boot-check') && $options->get('no-boot-check') ) {
    $plugin->nagios_exit( CRITICAL,
        'Error --boot-check and --no-boot-check specified at the same time' );
}

if ( $options->get('no-boot-check') ) {
    $bootcheck = 0;
}

#########
# Timeout

alarm $options->timeout;

verbose "Checking a $OSNAME system\n";

if ( $OSNAME eq 'linux' ) {

    if ( -r '/etc/issue' ) {

        my $header;
        my $TMP;

        open $TMP, q{<}, '/etc/issue'
          or $plugin->nagios_exit( CRITICAL,
            "Error opening /etc/issue: $OS_ERROR" );
        while (<$TMP>) {
            chomp;
            $header = $_;
            last;
        }
        close $TMP
          or $plugin->nagios_exit( CRITICAL,
            "Error closing /etc/issue: $OS_ERROR" );

        if ( $header =~ /Fedora/mxs ) {
            verbose "Fedora detected: using yum\n";
            check_running_kernel();
            check_yum();
        }
        elsif ( $header =~ /CentOS/mxs ) {
            verbose "CentOS detected: using yum\n";
            check_running_kernel();
            check_yum();
        }
        elsif ( $header =~ /Red\ Hat.*\ 4/mxs ) {
            verbose "RedHat 4 detected: using up2date\n";
            check_running_kernel();
            check_up2date();
        }
        elsif ( $header =~ /Red\ Hat.*\ 5/mxs ) {
            verbose "RedHat 5 detected: using yum\n";
            check_running_kernel();
            check_yum();
        }
        else {
            $plugin->nagios_exit( UNKNOWN, 'unknown Linux distribution' );
        }

        if ( $bootcheck && $wrong_kernel ) {
            $status = CRITICAL;
        }

        # Nagios::Plugin does not support the addition Nagios 3 status lines
        # -> we do it manually

        print 'CHECK_UPDATES '
          . $Nagios::Plugin::STATUS_TEXT{$status}
          . " - $exit_message |";

        for my $pdata ( @{ $plugin->perfdata } ) {
            print q{ } . $pdata->perfoutput;
        }

        print "\n";

        for my $package (@status_lines) {
            print "$package\n";
        }

        exit $status;

    }
    else {
        $plugin->nagios_exit( UNKNOWN,
            'Cannot detect Linux distribution (no /etc/issue file)' );
    }

}

1;
