#!/usr/bin/perl

# 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-2024 Matteo Corti <matteo@corti.li>
#
# 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.

use strict;
use warnings;

no warnings 'once';    ## no critic (TestingAndDebugging::ProhibitNoWarnings)

our $VERSION = '2.0.6';

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

Readonly our $EXIT_UNKNOWN                 => 3;
Readonly our $YUM_RETURN_ERROR             => 1;
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
  $debug_fh
  $help
  $options
  $plugin
  $security_plugin
  $status
  $threshold
  $wrong_kernel
  $yum_executable
  $errorlevel
  @status_lines
);
## use critic

##############################################################################
# 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( Monitoring::Plugin->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     : debug("some message string");
# Purpose   : write a debugging message
# Returns   : n/a
# Arguments : message : message string
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub debug {

    # arguments
    my $message = shift;

    if ( !defined $message ) {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            q{Internal error: not enough parameters for 'debug'} );
    }

    if ( defined $debug_fh ) {
        print {$debug_fh} "$message"
          or exit_with_error( Monitoring::Plugin->UNKNOWN,
            q{Cannot write do debug file} );
    }

    if ( defined $options && $options->get('debug') ) {
        print "[DBG] $message";    ## no critic (RequireCheckedSyscalls)
    }

    return;

}

##############################################################################
# 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     : get_path('program_name');
# Purpose   : retrieves the path of an executable file using the
#             'which' utility
# Returns   : the path of the program (if found)
# Arguments : the program name
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub get_path {

    my $prog = shift;
    my $path;

    my $which_command = "which $prog";
    my $which_output;

    verbose "Executing 'which $prog'\n", 2;

    #<<<
    open $which_output, q{-|}, "$which_command 2>&1"
      or $plugin->nagios_exit( $plugin->UNKNOWN,  "Cannot execute $which_command: $OS_ERROR" );
    while (<$which_output>) {
        verbose $_, 2;
        chomp;
        if ( !/^which:/mxs ) {
            $path = $_;
        }
    }
    if (  !( close $which_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( $plugin->UNKNOWN,
            "Error while closing pipe to $which_command: $OS_ERROR" );
    }
    #>>>
    return $path;

}

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

    run();
}

##############################################################################
# Usage     : whoami()
# Purpose   : retrieve the user running 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( Monitoring::Plugin->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( Monitoring::Plugin->UNKNOWN,
            "Error while closing pipe to whoami: $OS_ERROR\n" );

    }

    exit_with_error( Monitoring::Plugin->UNKNOWN, 'Cannot determine the user' );
    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 core
    $kernel =~ s/core-//imxs;

    # remove Xen
    $kernel =~ s/xen-//imxs;

    # remove plus
    $kernel =~ s/plus-//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 flavors

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

    # remove UEK
    $kernel =~ s/uek-//imxs;

    # remove ml, lt, aufs
    $kernel =~ s/ml-//imxs;
    $kernel =~ s/lt-//imxs;
    $kernel =~ s/aufs-//imxs;

    # Remove azure
    $kernel =~ s/azure-//imxs;

    return $kernel;

}

##############################################################################
# Usage     : run_command( );
# Purpose   : runs a command
# Returns   : command output (STDERR and STDOUT merged)
# Arguments : command
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub run_command {

    my $command = shift;

    # prepend the localization settings
    $command = 'LC_ALL=C ; ' . $command;

    my $command_output;
    my $output = q{};

    open $command_output, q{-|},
      "$command 2>&1"
      or $plugin->nagios_exit( $plugin->UNKNOWN,
        "Cannot execute $command: $OS_ERROR" );
    while (<$command_output>) {
        $output = $output . $_;
    }
    if (  !( close $command_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( $plugin->UNKNOWN,
            "Error while closing pipe to $command: $OS_ERROR" );
    }
    return $output;

}

##############################################################################
# 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-PAE",
            "$package-PAE-core", "$package-azure",
            "$package-core",     "$package-lt",
            "$package-lt-aufs",  "$package-ml",
            "$package-ml-aufs",  "$package-plus",
            "$package-smp",      "$package-uek",
            "$package-xen",      'ovzkernel',
            'vzkernel',
        )
      )

    {

        my $output;

        #<<<
        my $pid = open $output, q{-|}, "rpm -q $rpm" ## no critic (RequireBriefOpen)
          or exit_with_error( Monitoring::Plugin->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( Monitoring::Plugin->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];

    if ( !$installed ) {

# there are systems where the kernel is not installed with RPG (e.g., CentOS on RPi)
        verbose "Unable to detect installed kernel: skipping kernel checks\n";

    }
    else {

        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 arguments
# Returns   : the list of updates
# Arguments : string with the command line arguments
#           : (optional) yum command
# Throws    : n/a
# Comments  : n/a
# See also  : n/a

## no critic (ProhibitExcessComplexity)
sub run_yum {

    my $arguments   = shift;
    my $yum_command = shift;

    my $assume;
    if ( defined $options && defined $options->get('assumeyes') ) {
        $assume = 'assumeyes';
    }
    else {
        $assume = 'assumeno';
    }

    if ( ( -l $yum_executable ) and ( readlink($yum_executable) =~ /dnf5/mxs ) )
    {
        $errorlevel = q{};
    }
    else {
        $errorlevel = '--errorlevel=0';
    }

    # per default check updates
    # --assumeno to avoid a timeout when the GPG key is not present
    # we need to process STDERR to catch errors (e.g., missing GPG key)
    if ( !defined $yum_command ) {
        $yum_command = "check-update --$assume $errorlevel -q";
    }

    my $OUTPUT_HANDLER;
    my @updates;

    my $command = "$yum_executable $yum_command $arguments 2>&1";

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

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

        my $line = $_;

        debug $line;

        chomp $line;

        if ( !$line ) {
            next;
        }

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

        # The obsoleted packages are already listed
        if ( $line =~ /^Obsoleting/imxs ) {
            last;
        }

        # GPG key is missing
        if (   defined $options
            && !$options->get('assumeyes')
            && $line =~ /^Importing[ ]GPG[ ]key/imxs )
        {
            exit_with_error( Monitoring::Plugin->UNKNOWN,
                'Missing GPG key, run "dnf check-update" manually' );
        }

        # Lines to be ignored
        if (   $line =~ /^Last[ ]metadata[ ]expiration/imxs
            || $line =~ /^Loaded[ ]plugins:/imxs
            || $line =~ /^Loading[ ]mirror[ ]speeds/imxs
            || $line =~ /[ ].B\/s[ ][|][ ]/imxs
            || $line =~ /^No[ ]security[ ]updates/imxs
            || $line =~
            /^Repository[ ]'.*' is missing name in configuration, using id/imxs
            || $line =~
            /^Security:[ ].*is[ ]an[ ]installed[ ]security[ ]update/imxs
            || $line =~
            /^Security:[ ].*is[ ]the[ ]currently[ ]running[ ]version/imxs
            || $line =~ /^Update[ ]notice/imxs
            || $line =~ /^You[ ]should/imxs
            || $line =~ /^If[ ]you[ ]are/imxs
            || $line =~ /^To[ ]help[ ]pinpoint[ ]the[ ]issue/imxs
            || $line =~ /You[ ]must[ ]run[ ]this[ ]command[ ]as[ ]root/imxs
            || $line =~ /^No[ ]packages[ ]needed/imxs )
        {
            next;
        }

# After and including "Uploading Enabled Repositories Report" the output can be ignored
        if ( $line =~ /^Uploading[ ]Enabled[ ]Repositories[ ]Report/imxs ) {
            last;
        }

        $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( Monitoring::Plugin->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";
        }
    }
    elsif ( ( $CHILD_ERROR >> $BITS_PER_BYTE ) == $YUM_RETURN_ERROR ) {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            "Error while executing '$command'\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;

    verbose "Checking if YUM/DNF supports the --security option\n", 2;

    if ( $yum_executable =~ /dnf$/mxs ) {
        verbose "Using DNF: no security plugin necessary\n", 2;
        $security_plugin = 1;
    }
    else {

        my $pid = open $OUTPUT_HANDLER, q{-|},
          "$yum_executable --help 2>&1 | grep -q -- --security"
          or exit_with_error( Monitoring::Plugin->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( Monitoring::Plugin->UNKNOWN,
                "Error while closing pipe to rpm: $OS_ERROR\n" );

        }

        if ( $CHILD_ERROR == 0 ) {
            verbose "Using YUM: security plugin installed\n", 1;
            $security_plugin = 1;
        }
        else {
            verbose
"Using YUM and security plugin not installed: all updates will be considered security updates\n"
              ,;
        }
    }

    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();

    if ( $options->get('clean') ) {
        verbose 'Cleaning YUM/DNF caches';
        run_yum( '-q', 'clean all' );
    }

    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
"YUM does not distinguish security updates ignoring --security-only option\n";
            }

            if ( !$options->get('number-only') ) {

                # every update is critical since it could be a security problem
                verbose
"no security plugin: every update could be a security problem\n";
                $status = Monitoring::Plugin->CRITICAL;

            }
            else {
                verbose
"Security updates available but --number-only was specified\n";
            }

            verbose "DNF/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') ) {
                if ( $status != Monitoring::Plugin->CRITICAL ) {

     # if we have a critical status for something else the status stays critical
                    $status = Monitoring::Plugin->WARNING;
                }
            }
            if ( @outdated >= $options->get('critical') ) {
                $status = Monitoring::Plugin->CRITICAL;
            }

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

                # ignore any other update
                $status = Monitoring::Plugin->OK;
            }

     # --security generates a message on STDERR if no security update is present
            @security_updates = run_yum( $arguments . ' --security 2>&1' );

            if ( @security_updates > 0 ) {

                if ( !$options->get('number-only') ) {

                 # every update is critical since it could be a security problem
                    verbose
"no security plugin: every update could be a security problem\n";
                    $status = Monitoring::Plugin->CRITICAL;
                }
                else {
                    verbose
"Security updates available but --number-only was specified\n";
                }

                $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-security update';
                if ( @outdated > 1 ) {
                    $message = $message . q{s};
                }

                push @status_lines, @outdated;

            }

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

        }

    }
    else {
        $status  = Monitoring::Plugin->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     : 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(
                Monitoring::Plugin->UNKNOWN,
                'Cannot detect Linux distribution'
            );
        }

    }

    my $header;

    if ( -r $filename ) {

        my $TMP;

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

    }
    else {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            "Cannot detect Linux distribution (no $filename file)" );
    }

    return $header;

}

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

## no critic (ProhibitExcessComplexity)
sub run {

    $status = Monitoring::Plugin->OK;

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

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

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

    $options = Monitoring::Plugin::Getopt->new(
        usage   => 'Usage: %s [OPTIONS]',
        version => $VERSION,
        url     => 'https://github.com/matteocorti/check_updates',
        blurb   => 'Checks if RedHat or Fedora system is up-to-date',
    );

    $options->arg(
        spec => 'assumeyes',
        help =>
'Automatically answer yes for all dnf check-updates questions. Useful for importing GPG keys'
    );

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

    $options->arg(
        spec => 'boot-check-warning',
        help => 'Like --boot-check but state is warning instead of critical',
    );

    $options->arg(
        spec => 'debug|d',
        help => 'Enables debugging messages',
    );

    $options->arg(
        spec => 'debug-file=s',
        help => 'Write debugging messages to a file',
    );

    $options->arg(
        spec => 'ignore-gpg-issues',
        help => 'Disable GPG signature checking',
    );

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

    $options->arg(
        spec => 'clean',
        help => 'Cleans YUM/DNF caches',
    );

    $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->arg(
        spec => 'number-only|n',
        help =>
'consider the number of updates only (security updates are not automatically critical)',
    );

    $options->arg(
        spec => 'quiet',
        help => 'Do not print package list',
    );

    $options->getopts();

    my $debug_file = $options->get('debug-file');
    if ($debug_file) {
        ## no critic (InputOutput::RequireBriefOpen)
        open $debug_fh, q{>},
          $options->get('debug-file')
          or $plugin->nagios_exit( $plugin->UNKNOWN,
            "Cannot open debug file ($debug_file): $OS_ERROR" );
    }

    debug "check_updates version: $VERSION\n", 1;
    debug 'running as:            ' . whoami() . "\n";
    debug 'system info:           ' . run_command('uname -a'), 1;
    debug q{                       }
      . run_command(
'if [ -f /etc/os-release ] ; then cat /etc/os-release; else echo "/etc/os-release not found"; fi'
      ), 1;

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

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

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

    # check bootcheck consistencys
    if ( $options->get('boot-check') && $options->get('no-boot-check') ) {
        exit_with_error( Monitoring::Plugin->CRITICAL,
            'Error --boot-check and --no-boot-check specified at the same time'
        );
    }
    if ( $options->get('boot-check-warning') && $options->get('no-boot-check') )
    {
        exit_with_error( Monitoring::Plugin->CRITICAL,
'Error --boot-check-warning and --no-boot-check specified at the same time'
        );
    }

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

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

    debug "PATH = $ENV{PATH}\n";
    if ( defined $ENV{HOME} ) {
        debug "HOME = $ENV{HOME}\n";
    }
    if ( !defined( $ENV{PATH} ) || $ENV{PATH} eq q{} ) {
        local $ENV{PATH} = '/bin:/usr/bin';
        debug "  empty PATH, setting to $ENV{PATH}\n";
    }

    # check bootcheck consistencys
    if ( $options->get('number-only') && $options->get('security-only') ) {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            'Error --security-only and --number-only specified at the same time'
        );
    }

    ############
    # YUM or DNF

    if ( !defined $yum_executable ) {

        verbose 'Searching for DNF or YUM in ' . $ENV{'PATH'} . "\n", 1;

        # the yum executable is not mandated by the tests
        $yum_executable = get_path('dnf');

        if ( !defined $yum_executable ) {
            $yum_executable = get_path('yum');
        }

    }

    if ( !defined $yum_executable ) {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            "Cannot find DNF or YUM\n" );
    }

    #########
    # Arguments

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

    # Check if foreman-protector is installed
    if ( -e '/etc/yum/pluginconf.d/foreman-protector.conf' ) {
        $arguments = "$arguments --disableplugin=foreman-protector";
    }

    #########
    # 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";

        verbose "Using Yum or DNF\n";
        check_running_kernel();
        check_yum($arguments);

        if ( $bootcheck == 2 && $wrong_kernel ) {
            if ( $status != Monitoring::Plugin->CRITICAL ) {

                # do not change the exit level if already critical
                $status = Monitoring::Plugin->WARNING;
            }
        }
        elsif ( $bootcheck && $wrong_kernel ) {
            $status = Monitoring::Plugin->CRITICAL;
        }

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

        eval {    ## no critic (ErrorHandling::RequireCheckingReturnValueOfEval)
            ## no critic (Modules::RequireBarewordIncludes)
            require 'Monitoring/Plugin.pm';
            ## use critic (Modules::RequireBarewordIncludes)
            Monitoring::Plugin->import();
        };
        if ( !$EVAL_ERROR ) {
            #<<<
            print 'CHECK_UPDATES '  ## no critic (RequireCheckedSyscalls)
                . $Monitoring::Plugin::STATUS_TEXT{$status}
                    . " - $exit_message |";
            #>>>
        }
        else {
            #<<<
            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)
        #>>>

        if ( !$options->get('quiet') ) {
            for my $package (@status_lines) {
                #<<<
                print "$package\n"; ## no critic (RequireCheckedSyscalls)
                #>>>
            }
        }

        exit $status;

    }
    else {
        exit_with_error( Monitoring::Plugin->UNKNOWN,
            'Cannot detect Linux system' );
    }

    return;

}

1;
