dsa-check-packages: Ignore :$arch in package names of dpkg -l output
[mirror/dsa-nagios.git] / dsa-nagios-checks / checks / dsa-check-packages
1 #!/usr/bin/perl
2
3 # dsa-check-packages
4
5 # checks for obsolete/local and upgradeable packages.
6 #
7 # packages for the obsolete/local check can be ignored, by
8 # listing their full name in /etc/nagios/obsolete-packages-ignore
9 # or by having a regex (starting a line with "/") that matches
10 # the packagename in said file.
11 #
12 # Takes one optional argument, the location of the ignore file.
13
14
15 # Copyright (C) 2008, 2009 Peter Palfrader <peter@palfrader.org>
16 #
17 # Permission is hereby granted, free of charge, to any person obtaining
18 # a copy of this software and associated documentation files (the
19 # "Software"), to deal in the Software without restriction, including
20 # without limitation the rights to use, copy, modify, merge, publish,
21 # distribute, sublicense, and/or sell copies of the Software, and to
22 # permit persons to whom the Software is furnished to do so, subject to
23 # the following conditions:
24 #
25 # The above copyright notice and this permission notice shall be
26 # included in all copies or substantial portions of the Software.
27 #
28 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
30 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
33 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
36 use strict;
37 use warnings;
38 use English;
39
40 my $IGNORE = "/etc/nagios/obsolete-packages-ignore";
41 my $IGNORED = "/etc/nagios/obsolete-packages-ignore.d";
42
43 my %CODE = (
44         'OK'            => 0,
45         'WARNING'       => 1,
46         'CRITICAL'      => 2,
47         'UNKNOWN'       => 3
48 );
49 my $EXITCODE = 'OK';
50 sub record($) {
51         my ($newexit) = @_;
52         die "code $newexit not defined\n" unless defined $CODE{$newexit};
53
54         if ($CODE{$newexit} > $CODE{$EXITCODE}) {
55                 $EXITCODE = $newexit;
56         };
57 }
58
59
60
61 sub get_packages {
62         $ENV{'COLUMNS'} = 1000;
63         $ENV{'LC_ALL'} = 'C';
64         open(F, "dpkg --print-architecture|") or die ("Cannot run dpkg --print-architecture: $!\n");
65         my $arch = <F>;
66         chomp($arch);
67         close(F);
68
69
70         open(F, "dpkg -l|") or die ("Cannot run dpkg -l: $!\n");
71         my @lines = <F>;
72         close(F);
73         chomp(@lines);
74
75         shift @lines while ($lines[0] !~ /\+\+\+/);
76         shift @lines;
77
78         my %pkgs;
79         for my $line (@lines) {
80                 my ($state, $pkg, $version, undef) = split(/  */, $line);
81                 $pkg =~ s/\Q:$arch\E$//;
82                 $pkgs{$state}{$pkg} = { 'installed' => $version }
83         }
84
85         my $installed = $pkgs{'ii'};
86         delete $pkgs{'ii'};
87
88         open my $olderr, ">&STDERR"   or die "Can't dup STDERR: $!";
89         open     STDERR, ">/dev/null" or die "Can't dup STDOUT: $!";
90
91         open (F, "apt-cache policy ".(join(" ", keys(%$installed)))." |") or die ("Cannot run apt-cache policy: $!\n");
92         @lines = <F>;
93         close(F);
94         chomp(@lines);
95         open STDERR, ">&", $olderr  or die "Can't dup OLDERR: $!";
96
97         my $line;
98         my $pkgname = undef;
99         while (defined($line = shift @lines)) {
100                 if ($line =~ /^([^ ]*):$/) {
101                         $pkgname = $1;
102                 } elsif ($line =~ /^ +Installed: (.*)$/) {
103                         # etch dpkg -l does not print epochs, so use this info, it's better
104                         $installed->{$pkgname}{'installed'} = $1;
105                 } elsif ($line =~ /^ +Candidate: (.*)$/) {
106                         $installed->{$pkgname}{'candidate'} = $1;
107                 } elsif ($line =~ /^ +\*\*\*/) {
108                         my @l;
109                         @l = split(/ +/, $line);
110                         $line = shift @lines;
111                         @l = split(/ +/, $line);
112                         $installed->{$pkgname}{'origin'} = $l[2];
113                 }
114         }
115
116         my (%current, %obsolete, %outofdate);
117         for my $pkgname (keys %$installed) {
118                 my $pkg = $installed->{$pkgname};
119
120                 unless (defined($pkg->{'candidate'}) && defined($pkg->{'origin'})) {
121                         $obsolete{$pkgname} = $pkg;
122                         next;
123                }
124                         
125                 if ($pkg->{'candidate'} ne $pkg->{'installed'}) {
126                         $outofdate{$pkgname} = $pkg;
127                         next;
128                 };
129                 if ($pkg->{'origin'} eq '/var/lib/dpkg/status') {
130                         $obsolete{$pkgname} = $pkg;
131                         next;
132                 }
133                 $current{$pkgname} = $pkg;
134         }
135
136         $pkgs{'current'} = \%current;
137         $pkgs{'outofdate'} = \%outofdate;
138         $pkgs{'obsolete'} = \%obsolete;
139         return \%pkgs;
140 }
141
142 sub load_ignores {
143         my ($ignorefiles, $require_file) = @_;
144
145         my @ignores;
146
147         for my $ignoreitem (@$ignorefiles) {
148                 next if (!$require_file and ! -e $ignoreitem);
149
150                 my @filestoopen;
151                 if (-d $ignoreitem) {
152                         opendir(DIR, $ignoreitem) or die ("Cannot open dir $ignoreitem: $!\n");
153                         @filestoopen = readdir(DIR);
154                         closedir(DIR);
155
156                         @filestoopen = grep { -f ($ignoreitem.'/'.$_) } @filestoopen;
157                         @filestoopen = grep { /^([a-z0-9_.-]+)+[a-z0-9]+$/i } @filestoopen;
158                         @filestoopen = grep { !/dpkg-(old|dist|new|tmp)$/ } @filestoopen;
159                         @filestoopen = map { ($ignoreitem.'/'.$_) } @filestoopen;
160                 } else {
161                         push @filestoopen, $ignoreitem;
162                 }
163
164                 for my $f (@filestoopen) {
165                         open (F, "< $f") or die ("Cannot open $f: $!\n");
166                         push @ignores, <F>;
167                         close F;
168                 }
169         }
170         chomp(@ignores);
171         return \@ignores;
172 }
173
174 sub check_ignore {
175         my ($pkg, $ignores) = @_;
176
177         my $ignore_this = 0;
178         for my $ignore (@$ignores) {
179                 my $ig = $ignore;
180                 return 1 if ($ig eq $pkg);
181                 if (substr($ig,0,1) eq '/') {
182                         substr($ig, 0, 1, '');
183                         $ig =~ s,/$,,;
184                         return 1 if ($pkg =~ /$ig/);
185                 }
186         }
187         return 0
188 }
189
190 sub filter_ignored {
191         my ($packages, $ignores) = @_;
192
193         my $obs = $packages->{'obsolete'};
194
195         my (%ignored, %bad);
196         for my $pkg (keys %$obs) {
197                 if (check_ignore($pkg, $ignores)) {
198                         $ignored{$pkg} = $obs->{$pkg};
199                 } else {
200                         $bad{$pkg} = $obs->{$pkg};
201                 };
202         }
203         delete $packages->{'obsolete'};
204         $packages->{'obsolete'} = \%bad;
205         $packages->{'obsolete-ignored'} = \%ignored;
206 };
207
208 sub usage {
209         my ($fd, $exit) = @_;
210         print $fd "Usage: $PROGRAM_NAME [<ignorefile|dir> [<ignorefile|dir> ...]]\n";
211         exit $exit;
212 }
213
214 my $ignorefiles = [$IGNORE, $IGNORED];
215 my $ignorefile_userset = 0;
216 if (@ARGV >= 1) {
217         usage(\*STDOUT, 0) if ($ARGV[0] eq "-h");
218         usage(\*STDOUT, 0) if ($ARGV[0] eq "--help");
219         $ignorefile_userset = 1;
220         $ignorefiles = \@ARGV;
221 };
222
223 my $ignores = load_ignores($ignorefiles, $ignorefile_userset);
224 my $packages = get_packages();
225
226 filter_ignored($packages, $ignores);
227
228
229
230 my @reportform = (
231         { 'key' => 'obsolete',
232           'listpackages' => 1,
233           'long' => "%d local or obsolete packages: %s",
234           'short' => "%d obs/loc",
235           'perf' => "obs_loc=%d;1;5;0",
236           'status' => 'WARNING' },
237         { 'key' => 'outofdate',
238           'listpackages' => 1,
239           'long' => "%d out of date packages: %s",
240           'short' => "%d updates",
241           'perf' => "outdated=%d;1;5;0",
242           'status' => 'WARNING' },
243         { 'key' => 'current',
244           'listpackages' => 0,
245           'long' => "%d packages current.",
246           'short' => "%d ok",
247           'perf' => "current=%d;;;0",
248           'status' => 'OK' },
249         { 'key' => 'obsolete-ignored',
250           'listpackages' => 1,
251           'long' => "%d whitelisted local or obsolete packages: %s",
252           'short' => "%d obs/loc(ignored)",
253           'perf' => "obs_ign=%d;;;0",
254           'status' => 'OK' },
255         { 'key' => 'rc',
256           'listpackages' => 1,
257           'long' => "%d packages removed but not purged: %s",
258           'short' => "%d rc",
259           'perf' => "rm_unprg=%d;;;0",
260           'status' => 'OK' },
261         { 'key' => 'hi',
262           'listpackages' => 1,
263           'long' => "%d packages on hold: %s",
264           'short' => "%d hi",
265           'perf' => "hold=%d;;;0",
266           'status' => 'OK' },
267         { 'key' => 'pc',
268           'listpackages' => 1,
269           'long' => "%d packages requested to be purged but conffiles still installed: %s",
270           'short' => "%d pc",
271           'perf' => "prg_conf=%d;1;;0",
272           'status' => 'WARNING' },
273         );
274
275 my @longout;
276 my @perfout;
277 my @shortout;
278 for my $form (@reportform) {
279         my $pkgs = $packages->{$form->{'key'}};
280         delete $packages->{$form->{'key'}};
281         my $num = scalar keys %$pkgs;
282         push @perfout, sprintf($form->{'perf'}, $num);
283         next unless ($num > 0);
284         if ($form->{'listpackages'}) {
285                 my $list = join(", ", keys %$pkgs);
286                 push @longout, sprintf($form->{'long'}, $num, $list);
287         } else {
288                 push @longout, sprintf($form->{'long'}, $num);
289         };
290         push @shortout, sprintf($form->{'short'}, $num);
291         record($form->{'status'});
292 };
293 if (scalar keys %$packages) {
294         record('WARNING');
295         unshift @shortout, "unk: ".join(", ", keys %$packages);
296         for my $status (sort {$b cmp $a} keys %$packages) {
297                 my $pkgs = $packages->{$status};
298                 my $list = join(", ", keys %$pkgs);
299                 unshift @longout, "Unknown package status $status: $list";
300         };
301 }
302
303 my $shortout = $EXITCODE.": ".join(", ", @shortout);
304 my $longout = join("\n", @longout);
305 my $perfout = "|".join(" ", @perfout);
306
307 print $shortout;
308 print $longout,"\n";
309 print $perfout,"\n";
310
311 exit $CODE{$EXITCODE};