File: //proc/self/root/usr/lib/nagios/plugins/check_packages
#!/usr/bin/perl
# nagios: -epn
# dsa-check-packages
# checks for obsolete/local and upgradeable packages.
#
# packages for the obsolete/local check can be ignored, by
# listing their full name in /etc/nagios/obsolete-packages-ignore
# or by having a regex (starting a line with "/") that matches
# the packagename in said file.
#
# Takes one optional argument, the location of the ignore file.
# Copyright (C) 2008, 2009 Peter Palfrader <peter@palfrader.org>
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
use strict;
use warnings;
use English;
my $IGNORE = "/etc/nagios-plugins/obsolete-packages-ignore";
my $IGNORED = "/etc/nagios-plugins/obsolete-packages-ignore.d";
my %CODE = (
'OK' => 0,
'WARNING' => 1,
'CRITICAL' => 2,
'UNKNOWN' => 3
);
my $EXITCODE = 'OK';
sub record($) {
my ($newexit) = @_;
die "code $newexit not defined\n" unless defined $CODE{$newexit};
if ($CODE{$newexit} > $CODE{$EXITCODE}) {
$EXITCODE = $newexit;
};
}
sub get_packages {
$ENV{'COLUMNS'} = 1000;
$ENV{'LC_ALL'} = 'C';
open(F, "dpkg -l|") or die ("Cannot run dpkg: $!\n");
my @lines = <F>;
close(F);
chomp(@lines);
my $line;
my $has_arch = 0;
while (defined($line = shift @lines) && ($line !~ /\+\+\+/)) {
if ($line =~ /Architecture/) { $has_arch = 1; }
}
my %pkgs;
for $line (@lines) {
my ($state, $pkg, $version, $arch, undef) = split(/ */, $line);
$arch = '' unless $has_arch;
$pkgs{$state}{$pkg} = { 'installed' => $version, arch => $arch }
}
my $installed = $pkgs{'ii'};
delete $pkgs{'ii'};
my @installed_packages = keys(%$installed);
my @cmd = ("apt-cache", "policy", @installed_packages);
open my $olderr, ">&STDERR" or die "Can't dup STDERR: $!";
open STDERR, ">/dev/null" or die "Can't dup STDOUT: $!";
open (F, "-|", @cmd) or die ("Cannot run apt-cache policy: $!\n");
@lines = <F>;
close(F);
open STDERR, ">&", $olderr or die "Can't dup OLDERR: $!";
chomp(@lines);
my $pkgname = undef;
my $candidate_found = 0;
while (defined($line = shift @lines)) {
if ($line =~ /^([^ ]*):$/) {
# when we have multi-arch capable fu, we require that
# apt-cache policy output is in the same order as its
# arguments.
#
# We needs thi, because the output block in apt-cache
# policy does not show the arch:
#
# | weasel@stanley:~$ apt-cache policy libedit2:amd64
# | libedit2:
# | Installed: 2.11-20080614-5
# | Candidate: 2.11-20080614-5
#
# We replace the package name in the output with the
# one we asked for ($pkg:$arch) - but to match this up
# sanely we need the order to be correct.
#
# For squeeze systems (no m-a), apt-cache policy output
# is all different.
$pkgname = $1;
$candidate_found = 0;
if ($has_arch) {
my $from_list = shift @installed_packages;
next if ($pkgname eq $from_list); # no :$arch in pkgname we asked for
my $ma_fix_pkgname = $pkgname.':'.$installed->{$from_list}->{'arch'};
my $ma_fix_from_list = $from_list.':'.$installed->{$from_list}->{'arch'};
if ($pkgname eq $ma_fix_from_list || # e.g. ia32-libs-i386. dpkg -l: ia32-libs-i386, apt-cache policy: ia32-libs-i386:i386
$ma_fix_pkgname eq $from_list) {
$pkgname = $from_list;
} else {
die "Unexpected order mismatch in apt-cache policy output (apt-cache policy name: $pkgname - dpkg -l name: $from_list)\n";
}
}
} elsif ($line =~ /^ +Installed: (.*)$/) {
# etch dpkg -l does not print epochs, so use this info, it's better
$installed->{$pkgname}{'installed'} = $1;
# initialize security-update
$installed->{$pkgname}{'security-update'} = 0;
} elsif ($line =~ /^ +Candidate: (.*)$/) {
$installed->{$pkgname}{'candidate'} = $1;
} elsif ($line =~ / ([^ ]+) [0-9]+/) {
# check if the next lines show the sources of our candidate
if ($1 eq $installed->{$pkgname}{'candidate'}) {
$candidate_found = 1;
}
} elsif (($line =~ / +[0-9]+ [^ ]+\/(security\.([^ ]+\.)?debian\.org|debian-security).*\/updates\//) && $candidate_found ) {
$installed->{$pkgname}{'security-update'} = 1;
} elsif ($line =~ /^ +\*\*\*/) {
$line = shift @lines;
my @l = split(/ +/, $line);
$installed->{$pkgname}{'origin'} = $l[2];
$candidate_found = 0;
}
}
my (%current, %obsolete, %outofdate, %security_outofdate);
for my $pkgname (keys %$installed) {
my $pkg = $installed->{$pkgname};
unless (defined($pkg->{'candidate'}) && defined($pkg->{'origin'})) {
$obsolete{$pkgname} = $pkg;
next;
}
if ($pkg->{'candidate'} ne $pkg->{'installed'}) {
if ($pkg->{'security-update'}) {
$security_outofdate{$pkgname} = $pkg;
} else {
$outofdate{$pkgname} = $pkg;
}
next;
};
if ($pkg->{'origin'} eq '/var/lib/dpkg/status') {
$obsolete{$pkgname} = $pkg;
next;
}
$current{$pkgname} = $pkg;
}
$pkgs{'current'} = \%current;
$pkgs{'outofdate'} = \%outofdate;
$pkgs{'security_outofdate'} = \%security_outofdate;
$pkgs{'obsolete'} = \%obsolete;
return \%pkgs;
}
sub load_ignores {
my ($ignorefiles, $require_file) = @_;
my @ignores;
for my $ignoreitem (@$ignorefiles) {
next if (!$require_file and ! -e $ignoreitem);
my @filestoopen;
if (-d $ignoreitem) {
opendir(DIR, $ignoreitem) or die ("Cannot open dir $ignoreitem: $!\n");
@filestoopen = readdir(DIR);
closedir(DIR);
@filestoopen = grep { -f ($ignoreitem.'/'.$_) } @filestoopen;
@filestoopen = grep { /^([a-z0-9_.-]+)+[a-z0-9]+$/i } @filestoopen;
@filestoopen = grep { !/dpkg-(old|dist|new|tmp)$/ } @filestoopen;
@filestoopen = map { ($ignoreitem.'/'.$_) } @filestoopen;
} else {
push @filestoopen, $ignoreitem;
}
for my $f (@filestoopen) {
open (F, "< $f") or die ("Cannot open $f: $!\n");
push @ignores, <F>;
close F;
}
}
chomp(@ignores);
return \@ignores;
}
sub check_ignore {
my ($pkg, $ignores) = @_;
my $ignore_this = 0;
for my $ignore (@$ignores) {
my $ig = $ignore;
return 1 if ($ig eq $pkg);
if (substr($ig,0,1) eq '/') {
substr($ig, 0, 1, '');
$ig =~ s,/$,,;
return 1 if ($pkg =~ /$ig/);
}
}
return 0
}
sub filter_ignored {
my ($packages, $ignores) = @_;
my $obs = $packages->{'obsolete'};
my (%ignored, %bad);
for my $pkg (keys %$obs) {
if (check_ignore($pkg, $ignores)) {
$ignored{$pkg} = $obs->{$pkg};
} else {
$bad{$pkg} = $obs->{$pkg};
};
}
delete $packages->{'obsolete'};
$packages->{'obsolete'} = \%bad;
$packages->{'obsolete-ignored'} = \%ignored;
};
sub usage {
my ($fd, $exit) = @_;
print $fd "Usage: $PROGRAM_NAME [<ignorefile|dir> [<ignorefile|dir> ...]]\n";
exit $exit;
}
my $ignorefiles = [$IGNORE, $IGNORED];
my $ignorefile_userset = 0;
if (@ARGV >= 1) {
usage(\*STDOUT, 0) if ($ARGV[0] eq "-h");
usage(\*STDOUT, 0) if ($ARGV[0] eq "--help");
$ignorefile_userset = 1;
$ignorefiles = \@ARGV;
};
my $ignores = load_ignores($ignorefiles, $ignorefile_userset);
my $packages = get_packages();
filter_ignored($packages, $ignores);
my @reportform = (
{ 'key' => 'obsolete',
'listpackages' => 1,
'long' => "%d local or obsolete packages: %s",
'short' => "%d obs/loc",
'perf' => "obs_loc=%d;1;5;0",
'status' => 'WARNING' },
{ 'key' => 'outofdate',
'listpackages' => 1,
'long' => "%d out of date packages: %s",
'short' => "%d updates",
'perf' => "outdated=%d;1;5;0",
'status' => 'WARNING' },
{ 'key' => 'current',
'listpackages' => 0,
'long' => "%d packages current.",
'short' => "%d ok",
'perf' => "current=%d;;;0",
'status' => 'OK' },
{ 'key' => 'obsolete-ignored',
'listpackages' => 1,
'long' => "%d whitelisted local or obsolete packages: %s",
'short' => "%d obs/loc(ignored)",
'perf' => "obs_ign=%d;;;0",
'status' => 'OK' },
{ 'key' => 'rc',
'listpackages' => 1,
'long' => "%d packages removed but not purged: %s",
'short' => "%d rc",
'perf' => "rm_unprg=%d;;;0",
'status' => 'OK' },
{ 'key' => 'hi',
'listpackages' => 1,
'long' => "%d packages on hold: %s",
'short' => "%d hi",
'perf' => "hold=%d;;;0",
'status' => 'OK' },
{ 'key' => 'pc',
'listpackages' => 1,
'long' => "%d packages requested to be purged but conffiles still installed: %s",
'short' => "%d pc",
'perf' => "prg_conf=%d;1;;0",
'status' => 'WARNING' },
{ 'key' => 'security_outofdate',
'listpackages' => 1,
'long' => "%d packages with outstanding security updates: %s",
'short' => "%d security-updates",
'perf' => "security_outdated=%d;;1;0",
'status' => 'CRITICAL' },
);
my @longout;
my @perfout;
my @shortout;
for my $form (@reportform) {
my $pkgs = $packages->{$form->{'key'}};
delete $packages->{$form->{'key'}};
my $num = scalar keys %$pkgs;
push @perfout, sprintf($form->{'perf'}, $num);
next unless ($num > 0);
if ($form->{'listpackages'}) {
my $list = join(" ", keys %$pkgs);
push @longout, sprintf($form->{'long'}, $num, $list);
} else {
push @longout, sprintf($form->{'long'}, $num);
};
push @shortout, sprintf($form->{'short'}, $num);
record($form->{'status'});
};
if (scalar keys %$packages) {
record('WARNING');
unshift @shortout, "unk: ".join(" ", keys %$packages);
for my $status (sort {$b cmp $a} keys %$packages) {
my $pkgs = $packages->{$status};
my $list = join(" ", keys %$pkgs);
unshift @longout, "Unknown package status $status: $list";
};
}
my $shortout = $EXITCODE.": ".join(" ", @shortout);
my $longout = join("\n", @longout);
my $perfout = "|".join(" ", @perfout);
print $shortout,"\n";
print $longout,"\n";
print $perfout,"\n";
exit $CODE{$EXITCODE};