#!/usr/bin/perl -w
# nagios: -epn
#
# check_zypper - nagios plugin
#
# Copyright (C) 2008, Novell, Inc.
# Author: Lars Vogdt
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of the Novell nor the names of its contributors may be
#   used to endorse or promote products derived from this software without
#   specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# $Id$
#

use strict;
use warnings;
use Getopt::Long;
use vars qw($PROGNAME $VERSION $DEBUG);

# cleanup the environment
$ENV{'PATH'}='/bin:/usr/bin:/sbin:/usr/sbin:';
$ENV{'BASH_ENV'}='';
$ENV{'ENV'}='';

# constants
$PROGNAME="check_zypper";
$VERSION="1.22";
$DEBUG = 0;

# variables
our $zypper="/usr/bin/zypper";
our $zypperopt="--non-interactive --no-gpg-checks xml-updates";
our $sudo="/usr/bin/sudo";
our $refresh_wrapper="/usr/sbin/zypp-refresh-wrapper";
our $use_sudo="unset LANG; ";
our $releasefile="/etc/SuSE-release";
our $release="11.1";
our $dist="openSUSE";
our $patchlevel=0;
our ($opt_V, $opt_h, $opt_i, $opt_w, $opt_c, $opt_f, $opt_o, $opt_p, $opt_r, $opt_s, $opt_t, $opt_v);
our $exitcode=0;
our %ERRORS=('OK'=>0,'WARNING'=>1,'CRITICAL'=>2,'UNKNOWN'=>3,'DEPENDENT'=>4);
our %REVERSE=(4=>'DEPENDENT',3=>'UNKNOWN',2=>'CRITICAL',1=>'WARNING',0=>'OK');
our $TIMEOUT=120;
our @patchignore=();
our @packageignore=();

$opt_w="recommended,optional";
$opt_c="security";
$opt_f="$releasefile";
$opt_t="120";
$opt_v=0;
$opt_o=0;
$opt_p=1;
$opt_s=0;

#######################################################################
# Functions
#######################################################################

sub print_myrevision ($$) {
    my $commandName = shift;
    my $pluginRevision = shift;
    print "$commandName v$pluginRevision\n";
}

sub mysupport () {
    print "Please use https://bugzilla.novell.com to submit patches or suggest improvements.\n";
    print "Please include version information with all correspondence (when possible,\n";
    print "use output from the --version option of the plugin itself).\n";
}

sub usage ($) {
        my $format=shift;
        printf($format,@_);
        exit $ERRORS{'UNKNOWN'};
}

sub get_distribution($){
	my $file=shift || "$releasefile";
	open(RELEASE,"<$file") || warn ("Could not open $file\n");
	while (<RELEASE>){
    	if (/^SUSE Linux Enterprise/){
	        $dist="SLE";
	    } 
	    if (/^VERSION/){
	        ( $release ) = $_ =~ m/VERSION = (.*)/;
	    }
	    if (/^PATCHLEVEL/){
	        ( $patchlevel ) = $_ =~ m/PATCHLEVEL = (.*)/;
	    }
	}
	close(RELEASE);
        print STDERR "INFO: check_zypper version: $VERSION\n" if ($DEBUG);
	print STDERR "INFO: $dist,$release,$patchlevel\n" if ($DEBUG);
	return($dist,$release,$patchlevel);
}

sub print_usage () {
    print "This plugin checks for software updates on systems that use package\n";
    print "management systems based on the zypper command found in openSUSE.\n\n";
    print "It checks for security, recommended and optional patches and also for\n";
    print "optional package updates.\n\n";
    print "You can define the status by patch category. Use a commata to list more\n";
    print "than one category to a state. Possible values are recommended,optional,security\n";
    print "and packages\n\n";
    print "If you like to know the names of available patches and packages, use\n";
    print "the \"-v\" option.\n\n";
    print "Usage:\n";
    print "  $PROGNAME [-w <category>] [-c <category>] [-t <timeout>] [-v]\n";
    print "  $PROGNAME [-h | --help]\n";
    print "  $PROGNAME [-V | --version]\n";
    print "\n\nOptions:\n";
    print "  -c, --critical\n";
    print "      A patch with this category result in critical status.\n";
    print "      Default: $opt_c\n";
    print "  -f, --releasefile\n";
    print "      Use the given file to get informations about the distribution.\n";
    print "      Default: $releasefile\n";
    print "  -h, --help\n";
    print "      Print detailed help screen\n";
    print "  -i, --ignore <file>\n";
    print "      Ignore patches/packages that are mentioned in <file>\n";
    print "      Just list one patch/package per line - example:\n\n";
    print "      patch:libtiff-devel\n";
    print "      # comment\n";
    print "      package:libtiff3\n";
    print "      package:libtiff-devel\n\n";
    print "  -o, --ignore_outdated\n";
    print "      Don't warn if a repository is outdated.\n";
    print "  -p, --no_perfdata\n";
    print "      Print no perfdata\n";
    print "  -r, --refresh_repos\n";
    print "      Tries to refresh the repositories before checking for updates.\n";
    print "      Note: this maybe needs an entry in /etc/sudoers like:\n";
    print "          nagios ALL = NOPASSWD: /usr/bin/zypper ref\n";
    print "      (and additional lines for the \'-s\' Option) if no check-zypp-wrapper is available.\n";
    print "  -s, --use_sudo\n";
    print "      Zypper needs root privileges on some distributions (known: 10.1 and SLE10).\n";
    print "      You can enable the script to use $sudo to start zypper.\n";
    print "      But don't forget to enable nopasswd sudo for the user starting $PROGNAME\n";
    print "      Via lines like the two below on in /etc/sudoers:\n";
    print "          nagios ALL = NOPASSWD: /usr/bin/zypper sl, \\ \n";
    print "                       /usr/bin/zypper $zypperopt\n";
    print "  -t, --timeout\n";
    print "      Just in case of problems, let's not hang Nagios and define a timeout.\n";
    print "      Default value is: $opt_t seconds\n";
    print "  -v, --verbose_output\n";
    print "      Print more information (useful only with Nagios v3.x).\n";
    print "  -w, --warning\n";
    print "      A patch with this category result in warning status.\n"; 
    print "      Default: $opt_w\n";
    print "\n";
    print "  -V, --version\n";
    print "      Print version information\n";
    print "\n";
    print "  -d, --debug\n";
    print "      Print debug output to STDERR\n";
}

sub print_help {
    my $exit=shift || undef;
    print "Copyright (c) 2009, Novell, Inc.\n\n";
    print_usage();
    print "\n";
    mysupport();
    exit $exit if (defined($exit)); 
}

sub check_zypper(){
    if ( -x "$zypper" ){
        print STDERR "INFO: Trying $use_sudo $zypper sl 2>/dev/null 1>&2\n" if ( $DEBUG );
        return (system("$use_sudo $zypper sl 2>/dev/null 1>&2"));
    } else {
        return 1;
    }
}

sub refresh_zypper(){
    if ( -x "$refresh_wrapper" ){
        print STDERR "Trying: $refresh_wrapper 2>/dev/null 1>&2\n" if ( $DEBUG );
        return (system("$refresh_wrapper 2>/dev/null 1>&2"));
    }
	if ( -x "$zypper" ){
        print STDERR "Trying: $sudo $zypper ref 2>/dev/null 1>&2\n" if ( $DEBUG );
        my $res=system("$sudo $zypper ref 2>/dev/null 1>&2");
		if (( "$release" eq "10.2" ) || ("$dist" eq "SLE")){
			return 0 if ( $res );
		} else {
			return $res
		}
    } else {
        return 1;
    }
}

sub check_errorcode($){
    my $status=shift;
    my $level=0;
    my $returnvalue="OK";
    $returnvalue="WARNING" if ("$opt_w" =~ /$status/);
    $returnvalue="CRITICAL" if ("$opt_c" =~ /$status/);
    $level=$ERRORS{"$returnvalue"};
    $exitcode=$level if ( $level gt $exitcode);
    $returnvalue=$REVERSE{"$exitcode"};
    return "$returnvalue";
}

sub xml_re_escape($){
	my ($text) = @_;
	$text =~ s/&amp;/&/sg;
    $text =~ s/&lt;/</sg;
    $text =~ s/&gt;/>/sg;
    $text =~ s/&quot;/"/sg;
	$text =~ s/&apos;/'/sg;
	return $text;	
}

sub trim($){
	my ($text) = @_;
	$text =~ s/^\s+//;
	$text =~ s/\s+$//;
	return $text;
}

sub check(){
    my ($status,$ret_str,$error);
    my $secstr="";
    my $recstr="";
    my $optstr="";
    my $pacstr="";
    my $warnstr="";
    my $update_avail=0;
    my %packagelist;
    print STDERR "INFO: Trying $use_sudo $zypper $zypperopt\n" if ($DEBUG);
    if (open (FH, "$use_sudo $zypper $zypperopt 2>&1 |")) {
        while(<FH>){
            chomp; 
            my $category="unknown";
            print STDERR "LINE: $_\n" if ($DEBUG);
            # error handling
            return("UNKNOWN: ".xml_re_escape($_),"UNKNOWN") if (/not found on medium/);
            return("UNKNOWN: ".xml_re_escape($_),"UNKNOWN") if (/I\/O error: Can't provide/);
            return("UNKNOWN: ".xml_re_escape($_),"UNKNOWN") if (/Error message:/);
            return("UNKNOWN: ".xml_re_escape($_),"UNKNOWN") if (/A ZYpp transaction is already in progress./);
            return("UNKNOWN: ".xml_re_escape($_),"UNKNOWN") if (/System management is locked/);
            if ((/out-of-date/)&&(!$opt_o)){
               $error=check_errorcode("security");
               $warnstr="At least one of your Repositories is out of date. Please run \"zypper refresh\" as root to update it. ";
               $warnstr.="\n" if ($opt_v);
            }
			if (/<message type=\"warning\">(.*)<\/message>/){
				$update_avail=1;
				$error=check_errorcode("security");
				$warnstr=xml_re_escape($1)." ";
				$warnstr.="\n" if ($opt_v);
			}
            if (( "$release" eq "10.2" ) || (("$dist" eq "SLE") && ( "$release" eq "10" ))){
                my ($url,$name,$version,$category,$status)=split('\s*\|\s*',$_,5); # just for reference - perhaps we need the variables later
                if (defined($name)){
                  if (grep {/\Q$name\E/} @patchignore){
                    print STDERR "WARNING: ignoring $name as it is in \@patchignore\n" if ($DEBUG);
                    next;
                  }
                }
                $category="optional" if (/\|\s*optional\s*\|\s*Needed/);
                $category="recommended" if (/\|\s*recommended\s*\|\s*Needed/);
                $category="security" if (/\|\s*security\s*\|\s*Needed/);
                $packagelist{"$category"}{"$name"}{'category'}="$category" if defined($category);
                $packagelist{"$category"}{"$name"}{'status'}="$status" if defined($status);
                $packagelist{"$category"}{"$name"}{'name'}="$name" if defined($name);
            } else {
                if (/<update /){ 
                    my ($name)=$_=~/name="(.*?)"/;
                    if (/kind="patch"/){	# line contains patch
                        if (grep {/\Q$name\E/} @patchignore){
                            print STDERR "WARNING: ignoring $name as it is in \@patchignore\n" if ($DEBUG);
                            next;
                        }
                        $category="optional" if (/category="optional"/);
                        $category="recommended" if (/category="recommended"/);
                        $category="security" if (/category="security"/);
                    } elsif (/kind="package"/) {
                        if (grep {/\Q$name\E/} @packageignore){
                            print STDERR "WARNING: ignoring $name as it is in \@packageignore\n" if ($DEBUG);
                            next;
                        }
                        $category="package";
                    }	
                    $packagelist{"$category"}{"$name"}{'category'}="$category";
                    $packagelist{"$category"}{"$name"}{'name'}="$name";
                    $packagelist{"$category"}{"$name"}{'status'}="Needed";
                }
            }
        }
        if ($DEBUG){
            print STDERR "INFO: Packages    (paccount): ".scalar(keys %{($packagelist{'package'})})."\n";
            print STDERR "INFO: Optional    (optcount): ".scalar(keys %{($packagelist{'optional'})})."\n";
            print STDERR "INFO: Recommended (reccount): ".scalar(keys %{($packagelist{'recommended'})})."\n";
            print STDERR "INFO: Security    (seccount): ".scalar(keys %{($packagelist{'security'})})."\n";
            use Data::Dumper;
            print STDERR Data::Dumper->Dump([\%packagelist]);
        }
        if (defined($packagelist{'package'}) && (scalar(keys %{($packagelist{'package'})}) ne 0)){
            $update_avail=1;
            $error=check_errorcode("packages");
            $pacstr=scalar(keys %{($packagelist{'package'})})." package update(s);";
        }
        if (defined($packagelist{'optional'}) && (scalar(keys %{($packagelist{'optional'})}) ne 0)){
            $update_avail=1;
            $error=check_errorcode("optional");
            $optstr=scalar(keys %{($packagelist{'optional'})})." optional update(s);";
        }
        if (defined($packagelist{'recommended'}) && (scalar(keys %{($packagelist{'recommended'})}) ne 0)){
            $update_avail=1;
            $error=check_errorcode("recommended");
            $recstr=scalar(keys %{($packagelist{'recommended'})})." recommended update(s);";
        }
        if (defined($packagelist{'security'}) && (scalar(keys %{($packagelist{'security'})}) ne 0)){
            $update_avail=1;
            $error=check_errorcode("security");
            $secstr=scalar(keys %{($packagelist{'security'})})." security update(s);";
        }
        if ( $update_avail ){
            $ret_str.="$error : ".trim("$warnstr $secstr $recstr $optstr $pacstr")." ";
            my @packagelist=();
            if ( $opt_v ){
                foreach my $cat ('security','recommended','optional','package'){
                    for my $key (sort(keys %packagelist)){
                       if ( "$key" eq "$cat"){
                         for my $name (sort(keys %{$packagelist{$key}})){
                           if ( "$cat" eq "package"){
                             push @packagelist, $packagelist{$key}{$name}{'name'};
                           } else {
                             $ret_str.="\n$cat patch: ".$packagelist{$key}{$name}{'name'};
                           }
                         }
                       }
                    }
                }
                $ret_str.="\npackages: ".join(' ',@packagelist) if @packagelist;
                $ret_str.="\nIgnored Patches : ".join(' ',@patchignore)." " if @patchignore;
                $ret_str.="\nIgnored Packages: ".join(' ',@packageignore)." " if @packageignore;
            }
        } else {
            $error="OK";
            $ret_str="OK: no updates available ";
            if ( $opt_v ){
                $ret_str.="\nIgnored Patches : ".join(' ',@patchignore)." " if @patchignore;
                $ret_str.="\nIgnored Packages: ".join(' ',@packageignore)." " if @packageignore;
            }
        }
        $ret_str.="| security=".scalar(keys %{($packagelist{'security'})}).";;;; recommended=".scalar(keys %{($packagelist{'recommended'})}).";;;; optional=".scalar(keys %{($packagelist{'optional'})}).";;;; packages=".scalar(keys %{($packagelist{'package'})}).";;;;\n" if ($opt_p);
    }
    close(FH);
    return("$ret_str","$error");
}

#######################################################################
# Main
#######################################################################

Getopt::Long::Configure('bundling');
GetOptions(
           "V"   => \$opt_V, "version"         => \$opt_V,
           "h"   => \$opt_h, "help"            => \$opt_h,
           "d"   => \$DEBUG, "debug"           => \$DEBUG,
           "i=s" => \$opt_i, "ignore=s"        => \$opt_i,
           "w=s" => \$opt_w, "warning=s"       => \$opt_w,
           "c=s" => \$opt_c, "critical=s"      => \$opt_c,
           "f=s" => \$opt_f, "releasefile=s"   => \$opt_f,
           "o"   => \$opt_o, "ignore_outdated" => \$opt_o,
           "p:0" => \$opt_p, "no_perfdata:0"   => \$opt_p,
           "r"   => \$opt_r, "refresh_repos"   => \$opt_r,
           "t=i" => \$opt_t, "timeout=i"       => \$opt_t,
           "v"   => \$opt_v, "verbose_output"  => \$opt_v,
           "s"   => \$opt_s, "use_sudo"        => \$opt_s) or print_help(2);

$TIMEOUT=$opt_t if ($opt_t);

# Just in case of problems, let's not hang Nagios
$SIG{'ALRM'} = sub {
    print "UNKNOWN - Plugin timed out\n";
    exit $ERRORS{"UNKNOWN"};
};
alarm($TIMEOUT);

if ($opt_V) {
    print_myrevision($PROGNAME,"$VERSION");
    exit $ERRORS{'OK'};
}

($dist,$release,$patchlevel)=get_distribution("$opt_f");

$zypperopt="--non-interactive --no-gpg-checks list-updates" if ("$release" eq "10.2");
$zypperopt="--xmlout --non-interactive list-updates -t package -t patch" if ($release gt 11.1);

if ("$dist" eq "SLE"){
    if (("$release" eq "10") && ($patchlevel gt 0)){
        $zypperopt="--non-interactive --no-gpg-checks --terse list-updates";
    } else {
        $zypperopt="--xmlout --non-interactive list-updates -t package -t patch";
    }
}

$use_sudo.="$sudo" if ($opt_s);

if ($opt_h) {
    print_help();
    exit $ERRORS{'OK'};
}

if ($opt_i){
    if (! -r "$opt_i" ){
        print "Updates CRITICAL - can't find file '$opt_i' - perhaps you should not use option '-i'?\n";
        exit $ERRORS{"CRITICAL"};
    } else {
		open(IGNORES,"<$opt_i") or die "Could not open $opt_i: $!\n";
        print "INFO: Ignoring the following patches/packages:\n" if ($DEBUG);
        while(<IGNORES>){
            next if /^#/;
            next if /^\s*$/;
            chomp;
            if ((/^patch:/) || (/^Patch:/)){
				my ($foo,$toadd)=split(':', $_, 2 );
				$toadd =~ s/\s*//g; # Patch names have no whitespaces
				print "INFO: + Patch  : $toadd\n" if ($DEBUG);
				push @patchignore,"$toadd";
			} elsif ((/^package:/) || (/^Package:/)){
				my ($foo,$toadd)=split(':', $_, 2 );
				$toadd =~ s/\s*//g; # Package names have no whitespaces
				print "INFO: + Package: $toadd\n" if ($DEBUG);
				push @packageignore,"$toadd";
			}
        }
        close(IGNORES);
    }
}

if ($opt_r){
    if ( refresh_zypper() ){
        print "Updates UNKNWON - system does not allow to refresh the repositories\n";
        exit $ERRORS{"UNKNOWN"};
    }
}
alarm(0);

if ( check_zypper() ){
    print "Updates UNKNOWN - system does not allow to execute zypper\n";
    exit $ERRORS{"UNKNOWN"};
} else {
    my ($ret_str,$error)=check();
    print "Updates $ret_str";
    print STDERR "INFO: Exit-Code: ".$exitcode."\n" if ($DEBUG);
    exit $exitcode;
}
