#!/usr/bin/perl

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

# nagios: -epn

package main;

# 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-2014, 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 1400 2014-12-28 15:26:51Z corti $
#   $Revision: 1400 $
#   $HeadURL: https://svn.id.ethz.ch/nagios_plugins/check_updates/check_updates $
#   $Date: 2014-12-28 16:26:51 +0100 (Sun, 28 Dec 2014) $

use 5.00800;

use strict;
use warnings;

our $VERSION = '1.6.9';

use Carp;
use English qw(-no_match_vars);
use Nagios::Plugin::Getopt;
use Nagios::Plugin::Threshold;
use Nagios::Plugin;
use POSIX qw(uname);
use Readonly;

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

# 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...
#
## no critic (ProhibitPackageVars)
use vars qw(
  $bootcheck
  $exit_message
  $help
  $options
  $plugin
  $security_plugin
  $status
  $threshold
  $wrong_kernel
  $yum_executable
  @status_lines
);
## use critic

# the script is declared as a package so that it can be unit tested
# but it should not be used as a module
if ( !caller ) {

    # set the yum executable (the value can be overridden in the
    # tests to a mock
    $yum_executable = 'yum';

    run();
}

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

##############################################################################
# Usage     : exit_with_error( $status, $message)
# Purpose   : if a plugin object is available exits via ->nagios_exit
#             otherwise prints to the shell and exit normally
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub exit_with_error {

    my $status  = shift;
    my $message = shift;

    if ($plugin) {
        $plugin->nagios_exit( $status, $message );
    }
    else {
        #<<<
        print "Error: $message"; ## no critic (RequireCheckedSyscalls)
        #>>>
        exit $status;
    }

    return;

}

##############################################################################
# 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 exit_with_error( UNKNOWN, "Cannot determine the user: $OS_ERROR" );
    while (<$output>) {
        chomp;
        return $_;
    }
    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
        exit_with_error( UNKNOWN,
            "Error while closing pipe to whoami: $OS_ERROR\n" );

    }

    exit_with_error( 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 ) {
        exit_with_error( UNKNOWN,
            q{Internal error: not enough parameters for 'verbose'} );
    }

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

    # we check if options is defined as the it could be undefined if
    # running from a unit test
    if ( defined $options && $level < $options->verbose ) {
        #<<<
        print $message; ## no critic (RequireCheckedSyscalls)
        #>>>
    }

    return;

}

##############################################################################
# Usage     : versioncmp
# Purpose   : Sort revision-like numbers
# Returns   : -1, 0, or 1 depending on whether the left argument is less than,
#             equal to, or greater than the right argument.
# Arguments : Globally defined $a and $b as the versions to compare
# Throws    : n/a
# Comments  : Included (and adapted) from Sort::Version  to remove the
#             dependency on Sort::Version which is not available per default
#             on RH systems
# See also  : Sort::Version on CPAN
sub versioncmp {

    my @A = ( $a =~ /([-.]|\d+|[^-.\d]+)/gxms );
    my @B = ( $b =~ /([-.]|\d+|[^-.\d]+)/gxms );

    my ( $A, $B );
    while ( @A and @B ) {

        $A = shift @A;
        $B = shift @B;

        ## no critic (ProhibitCascadingIfElse)
        if ( $A eq q{-} and $B eq q{-} ) {
            next;
        }
        elsif ( $A eq q{-} ) {
            return -1;    ## no critic (ProhibitMagicNumbers)
        }
        elsif ( $B eq q{-} ) {
            return 1;
        }
        elsif ( $A eq q{.} and $B eq q{.} ) {
            next;
        }
        elsif ( $A eq q{.} ) {
            return -1;    ## no critic (ProhibitMagicNumbers)
        }
        elsif ( $B eq q{.} ) {
            return 1;
        }
        elsif ( $A =~ /^\d+$/xms and $B =~ /^\d+$/xms ) {
            if ( $A =~ /^0/xms || $B =~ /^0/xms ) {
                return $A cmp $B if $A cmp $B;
            }
            else {
                return $A <=> $B if $A <=> $B;
            }
        }
        else {
            $A = uc $A;
            $B = uc $B;
            return $A cmp $B if $A cmp $B;
        }
        ## use critic (ProhibitCascadingIfElse)

    }
    return @A <=> @B;
}

##############################################################################
# Usage     : $kernel_version = clean_kernel_version( $kernel_version )
# Purpose   : removes everything but the version number from the kernel
#             version (e.g., PAE, xen, ...)
# Returns   : kernel version
# Arguments : kernel_version : output of uname
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
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 openvz
    $kernel =~ s/o?vzkernel-//imxs;

    # remove architecture

    $kernel =~ s/[.](i[3-6]86|ppc|x86_64)$//mxs;

    # remove smp

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

    # remove RHEL, CentOS, Scientific Linux and Fedora flavours

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

    # remove UEK
    $kernel =~ s/uek-//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 {

    # return if running in openvz client: /proc/vz is always present but
    # /proc/bc only exists on node, but not inside container.
    if ( -d '/proc/vz' && !( -d '/proc/bc' ) ) {
        return;
    }

    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",
            "$package-uek", 'ovzkernel',    'vzkernel'
        )
      )

    {

        my $output;

        #<<<
        my $pid = open $output, q{-|}, "rpm -q $rpm" ## no critic (RequireBriefOpen)
          or exit_with_error( 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
            exit_with_error( 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-update with the supplied command line argumens
# Returns   : the list of updates
# Arguments : string with the command line arguments
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub run_yum {

    my $arguments = shift;

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

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

    verbose qq{Running "$command"\n}, 1;

    #<<<
    my $pid = open $OUTPUT_HANDLER, q{-|}, $command ## no critic (RequireBriefOpen)
      or exit_with_error( UNKNOWN, "Cannot list updates: $OS_ERROR" );
    #>>>
    while (<$OUTPUT_HANDLER>) {

        my $line = $_;

        verbose $line, 1;

        chomp $line;

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

        if ($blank_line) {

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

            $line =~ s{[ ].*}{}mxs;

            push @updates, $line;
        }

    }

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

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

    }

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

    return @updates;

}

##############################################################################
# Usage     : check_security_option
# Purpose   : checks if you supports --security (in older versions required
#             a plugin)
# Returns   : 1 if --security is supported, undef otherwise
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub check_security_option {

    my $OUTPUT_HANDLER;

    my $pid = open $OUTPUT_HANDLER, q{-|}, 'yum --help | grep -q -- --security'
      or
      exit_with_error( UNKNOWN, "Cannot check for security option: $OS_ERROR" );

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

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

    }

    if ( $CHILD_ERROR == 0 ) {
        verbose "Security plugin installed\n";
        $security_plugin = 1;
    }

    return;

}

##############################################################################
# Usage     : check_yum();
# Purpose   : checks a yum based system for updates
# Returns   : n/a
# Arguments : string with the command line arguments
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
## no critic (ProhibitExcessComplexity)
sub check_yum {

    my $arguments = shift;

    my $message;

    check_security_option();

    my @outdated = run_yum($arguments);
    my @security_updates;

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

    if ( @outdated > 0 ) {

        if ( !$security_plugin ) {

            if ( $options->get('security-only') ) {
                verbose
"up2date does not distinguish security updates ignoring --security-only option\n";
            }

            # 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 {

            if ( @outdated >= $options->get('warning') ) {
                $status = WARNING;
            }
            if ( @outdated >= $options->get('critical') ) {
                $status = CRITICAL;
            }

            if ( $options->get('security-only') ) {

                # ignore any other update
                $status = OK;
            }

            @security_updates = run_yum( '--security ' . $arguments );

            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';
    }

    if ($security_plugin) {
        $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;

}
## use critic (ProhibitExcessComplexity)

##############################################################################
# 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' ) {
        exit_with_error( 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    ## no critic (RequireBriefOpen)
      or exit_with_error( UNKNOWN, "Cannot list updates: $OS_ERROR" );

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

    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
        exit_with_error( UNKNOWN,
            "Error while closing pipe to yum (listing updates): $OS_ERROR\n" );

    }

    my $message;

    if ( @status_lines > 0 ) {

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

        if ( @status_lines >= $options->get('warning') ) {
            $status = WARNING;
        }
        if ( @status_lines >= $options->get('critical') ) {
            $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;

}

##############################################################################
# Usage     : my $system = get_os_name_and_version();
# Purpose   : returns the name of the system by parsing $VERSION_FILE
# Returns   : name of the system
# Arguments : n/a
# Throws    : CRITICAL if not able to read $VERSION_FILE
# Comments  : n/a
# See also  : n/a
sub get_os_name_and_version {

    my $filename = shift;
    if ( !$filename ) {

        if ( -r '/etc/system-release' ) {
            $filename = '/etc/system-release';
        }
        elsif ( -r '/etc/redhat-release' ) {
            $filename = '/etc/redhat-release';
        }
        else {
            exit_with_error( UNKNOWN, 'Cannot detect Linux distribution' );
        }

    }

    my $header;

    if ( -r $filename ) {

        my $TMP;

        open $TMP, q{<}, $filename
          or exit_with_error( CRITICAL, "Error opening $filename: $OS_ERROR" );
        while (<$TMP>) {
            chomp;
            $header = $_;
            last;
        }
        close $TMP
          or exit_with_error( CRITICAL, "Error closing $filename: $OS_ERROR" );

    }
    else {
        exit_with_error( UNKNOWN,
            "Cannot detect Linux distribution (no $filename file)" );
    }

    return $header;

}

##############################################################################
# Usage     : my $updater = get_updated()
# Purpose   : returns the name of updating system (yum or up2date)
# Returns   : name of updating system or undef if not found
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub get_updater {

    my $name = shift;

    if (   $name =~ /Fedora/mxs
        || $name =~ /CentOS/mxs
        || $name =~ /PUIAS/mxs
        || $name =~ /Scientific[ ]Linux/mxs
        || $name =~ /Red[ ]Hat.*[ ][567]/mxs
        || $name =~ /Oracle/mxs
        || $name =~ /Amazon/mxs )
    {
        return 'yum';
    }
    elsif ( $name =~ /Red[ ]Hat.*[ ]4/mxs ) {
        return 'up2date';
    }
    return;

}

##############################################################################
# Usage     : run();
# Purpose   : main method
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub run {

    $status = OK;

    ##############################################################################
    # 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 non-security updates are available',
        default => 0
    );

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

    $options->arg(
        spec => 'security-only',
        help => 'Ignores non-security updates',
    );

    $options->arg(
        spec => 'yum-arguments|a=s',
        help => 'specific Yum arguments as STRING',

    );

    $options->getopts();

    ###############
    # Sanity checks

    if ( $options->get('warning') > $options->get('critical') ) {
        exit_with_error( UNKNOWN,
                q{'critical' (}
              . $options->get('critical')
              . q{) must not be lower than 'warning' (}
              . $options->get('warning')
              . q{)} );
    }

    $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') ) {
        exit_with_error( CRITICAL,
            'Error --boot-check and --no-boot-check specified at the same time'
        );
    }

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

    #########
    # Arguments

    my $arguments = q{};
    if ( $options->get('yum-arguments') ) {
        $arguments = $options->get('yum-arguments');
    }

    #########
    # Timeout

    alarm $options->timeout;

    verbose "Checking a $OSNAME system\n";

    if ( $OSNAME eq 'linux' ) {

        my $name = get_os_name_and_version();
        verbose "Running on $name\n";

        my $updater = get_updater($name);

        if ( $updater eq 'yum' ) {
            verbose "Using Yum\n";
            check_running_kernel();
            check_yum($arguments);
        }
        elsif ( $updater eq 'up2date' ) {
            verbose "Using up2date\n";

            if ( $options->get('security-only') ) {
                verbose
"up2date does not distinguish security updates ignoring --security-only option\n";
            }

            check_running_kernel();
            check_up2date();
        }
        else {
            exit_with_error( 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 '  ## no critic (RequireCheckedSyscalls)
            . $Nagios::Plugin::STATUS_TEXT{$status}
            . " - $exit_message |";
        #>>>
        for my $pdata ( @{ $plugin->perfdata } ) {
            #<<<
            print q{ } . $pdata->perfoutput; ## no critic (RequireCheckedSyscalls)
            #>>>
        }

        #<<<
        print "\n"; ## no critic (RequireCheckedSyscalls)
        #>>>
        for my $package (@status_lines) {
            #<<<
            print "$package\n"; ## no critic (RequireCheckedSyscalls)
            #>>>
        }

        exit $status;

    }
    else {
        exit_with_error( UNKNOWN, 'Cannot detect Linux system' );
    }

    return;

}

1;
