1 #! /usr/bin/perl |
|
2 # source: https://ssl.schlittermann.de/hg/ius/nagios/nagios-plugin-amanda-client |
|
3 |
|
4 use 5.014; |
|
5 use strict; |
|
6 use warnings; |
|
7 use Getopt::Long; |
|
8 use POSIX; |
|
9 use File::Spec::Functions; |
|
10 use Data::Dumper; |
|
11 use File::Find; |
|
12 use Carp; |
|
13 use Pod::Usage; |
|
14 use Const::Fast; |
|
15 use if $ENV{DEBUG} => 'Smart::Comments'; |
|
16 use if $^V >= v5.18 => (experimental => qw/smartmatch/); |
|
17 |
|
18 const my $NAME => 'AMANDA-CLIENT'; |
|
19 const my $USER => 'backup'; |
|
20 const my $CFDIR => '/etc/amanda'; |
|
21 |
|
22 sub su; |
|
23 sub find_tool; |
|
24 sub check_perms; |
|
25 sub config_names; |
|
26 sub get_devices; |
|
27 |
|
28 sub amchecks; |
|
29 sub compare_lists; |
|
30 |
|
31 sub main; |
|
32 |
|
33 sub OK; |
|
34 sub WARNING; |
|
35 sub CRITICAL; |
|
36 sub UNKNOWN; |
|
37 sub verbose; |
|
38 sub unique { my %h; @h{@_} = (); return keys %h } |
|
39 |
|
40 local $SIG{__DIE__} = sub { UNKNOWN @_ unless $^S }; |
|
41 |
|
42 # this we need for testing only, if this file gets |
|
43 # included as a module |
|
44 sub import { |
|
45 no strict 'refs'; |
|
46 *{"$_[0]::verbose"} = sub { }; |
|
47 } |
|
48 |
|
49 |
|
50 exit main @ARGV if not caller; |
|
51 |
|
52 #---- |
|
53 |
|
54 sub main { |
|
55 my @opt_exclude; |
|
56 my $opt_verbose = 0; |
|
57 |
|
58 GetOptions( |
|
59 'e|x|exclude=s@' => \@opt_exclude, |
|
60 'h|help' => sub { pod2usage(-verbose => 1, -exit => 0) }, |
|
61 'm|man' => sub { pod2usage(-verbose => 2, -exit => 0) }, |
|
62 'v|verbose' => \$opt_verbose, |
|
63 ) or pod2usage; |
|
64 |
|
65 *::verbose = $opt_verbose ? sub { say '# ', @_ } : sub { }; |
|
66 |
|
67 verbose('setting working directory to /, this avoids permission problems'); |
|
68 chdir '/'; |
|
69 |
|
70 # test needs to be run as root:* or as backup:backup |
|
71 # change to backup if still root |
|
72 su $USER if $> == 0; |
|
73 |
|
74 # amservice needs to be suid root, but executable |
|
75 # by the backup user/group |
|
76 verbose q{checking permissions for `amservice'}; |
|
77 eval { check_perms find_tool('amservice'), 04750, 'root', $) } |
|
78 or UNKNOWN $@; |
|
79 |
|
80 # find the backup sets we know about |
|
81 # here we suppose that it's possible to find strings like |
|
82 # 'conf "foo"' in files named 'amanda-client.conf' below /etc/amanda |
|
83 |
|
84 verbose qq{find config names from $CFDIR}; |
|
85 my @confs = sort +unique eval { config_names $CFDIR } |
|
86 or UNKNOWN $@; |
|
87 |
|
88 eval { amchecks @confs } or CRITICAL $@; |
|
89 |
|
90 my @dles = |
|
91 eval { compare_lists confs => \@confs, exclude => \@opt_exclude, } |
|
92 or CRITICAL $@; |
|
93 OK 'config: ' . join(', ', @confs), @dles; |
|
94 |
|
95 # never reached |
|
96 return 0; |
|
97 } |
|
98 |
|
99 # compare the file systems |
|
100 # get a list of file system |
|
101 sub get_devices { |
|
102 open(my $fh, '/proc/filesystems'); |
|
103 my @types = map { /^\s+(\S+)/ ? $1 : () } <$fh>; |
|
104 my @df = (df => '-P', map { -t => $_ } @types); |
|
105 map { [$_, (stat)[0]] } map { (split ' ', $_)[5] } grep { /^\// } `@df`; |
|
106 } |
|
107 |
|
108 sub su { |
|
109 my $user = shift; |
|
110 my $group = (getgrnam $user)[0]; |
|
111 my $uid = getpwnam $user; |
|
112 my $gid = getgrnam $group; |
|
113 |
|
114 my @groups; |
|
115 |
|
116 setgrent; |
|
117 my @rc; |
|
118 while (my @g = getgrent) { |
|
119 push @groups, $g[2] if $user ~~ [split ' ', $g[3]]; |
|
120 } |
|
121 endgrent; |
|
122 $) = "@groups"; |
|
123 |
|
124 verbose "su to $uid:$gid"; |
|
125 |
|
126 # during testing |
|
127 return ($uid, $gid) if $ENV{HARNESS_ACTIVE}; |
|
128 |
|
129 setgid $gid; |
|
130 setuid $uid; |
|
131 } |
|
132 |
|
133 sub find_tool { |
|
134 my $name = shift; |
|
135 my @rc = grep { -f -x } map { catfile $_, $name } split /:/, $ENV{PATH} |
|
136 or die "Can't find executable `$name' in $ENV{PATH}\n"; |
|
137 $rc[0]; |
|
138 } |
|
139 |
|
140 sub check_perms { |
|
141 my ($file, $mode, $owner, $group) = @_; |
|
142 |
|
143 $owner = getpwuid $owner if $owner ~~ /^\d+$/; |
|
144 |
|
145 $group = getgrgid +(split ' ', $group)[0] |
|
146 if $group ~~ /^[\d\s]+$/; |
|
147 |
|
148 stat $file or croak "Can't stat `$file': $!\n"; |
|
149 |
|
150 eval { |
|
151 my $f_owner = getpwuid +(stat _)[4] or die $!; |
|
152 my $f_group = getgrgid +(stat _)[5] or die $!; |
|
153 my $f_mode = (stat _)[2] & 07777 or die $!; |
|
154 |
|
155 my $msg = |
|
156 sprintf "need: 0%04o root:%s, got: 0%04o %s:%s\n", |
|
157 $mode, $group, $f_mode, $f_owner, $f_group; |
|
158 |
|
159 if (-f '/etc/debian_version') { |
|
160 $msg .= sprintf "try dpkg-statoverride --update --add root %s 0%04o %s\n", |
|
161 $group, $mode, $file; |
|
162 } |
|
163 |
|
164 die $msg unless $f_owner eq $owner; |
|
165 die $msg unless $f_group eq $group; |
|
166 die $msg unless $f_mode == $mode; |
|
167 }; |
|
168 die "wrong permissions for `$file', $@" if $@; |
|
169 1; |
|
170 } |
|
171 |
|
172 sub config_names { |
|
173 my $dir = shift; |
|
174 my @configs = (); |
|
175 find( |
|
176 sub { |
|
177 -f and /^amanda-client\.conf$/ or return; |
|
178 open(my $fh, '<', $_) |
|
179 or die "Can't open $File::Find::name: $!\n"; |
|
180 push @configs, map { /^conf\s+"(.+?)"/ ? $1 : () } <$fh>; |
|
181 }, |
|
182 $dir |
|
183 ); |
|
184 |
|
185 die |
|
186 "no configs found below $dir (amanda-client.conf needs need `conf \"foo\"' line)\n" |
|
187 if not @configs; |
|
188 return @configs; |
|
189 } |
|
190 |
|
191 sub _amcheck { |
|
192 |
|
193 #config: daily |
|
194 #CHECKING |
|
195 # |
|
196 #Amanda Backup Client Hosts Check |
|
197 #-------------------------------- |
|
198 #Client check: 1 host checked in 2.242 seconds. 0 problems found. |
|
199 # |
|
200 #(brought to you by Amanda 3.3.1) |
|
201 #The check is finished |
|
202 |
|
203 my $conf = shift; |
|
204 my $o = qx(amdump_client --config '$conf' check 2>&1); |
|
205 |
|
206 $o =~ /^config:\s+ |
|
207 (BUSY Amanda is busy, retry later)/smx |
|
208 and WARNING "$1\n"; |
|
209 |
|
210 $o =~ /^config:\s+$conf\n |
|
211 CHECKING\n |
|
212 .*\n |
|
213 Client.check:.1.host.checked.in.\d+\.\d+.seconds\.\s+0.problems.found\.\n |
|
214 .*\n |
|
215 The.check.is.finished$ |
|
216 /smx or UNKNOWN "unexpected output from check:\n$o"; |
|
217 } |
|
218 |
|
219 sub amchecks { |
|
220 my @errors = (); |
|
221 foreach my $conf (@_) { |
|
222 eval { _amcheck $conf } or push @errors, $@; |
|
223 } |
|
224 die @errors if @errors; |
|
225 return 1; |
|
226 } |
|
227 |
|
228 sub _amlist { |
|
229 |
|
230 # return a list of [ name, dev ] tupels. |
|
231 # name: the name of the disk/device |
|
232 # dev: the local device id (stat)[0] |
|
233 # iff the inum of $name != 2, it's not the top directory |
|
234 # and we set the device id to -1, since $name does not stand for a whole |
|
235 # device |
|
236 my $conf = shift; |
|
237 chomp((undef, my @dles) = qx(amdump_client --config '$conf' list)); |
|
238 return map { [$_, (stat $_)[1] == 2 ? (stat $_)[0] : -1] } @dles; |
|
239 } |
|
240 |
|
241 sub compare_lists { |
|
242 my %arg = @_; |
|
243 my @confs = @{ $arg{confs} } or croak 'missing list of confs'; |
|
244 |
|
245 # magic: if there is no config with the exclude, generate a list of config:exclude |
|
246 my @exclude = map { /^[^\/]+?:/ ? $_ : do { my $x = $_; map { "$_:$x" } @confs } } |
|
247 @{ $arg{exclude} }; |
|
248 ### exclude:@exclude |
|
249 |
|
250 WARNING |
|
251 "excluded filesystem(s) @$_ does not exist, update the config please!\n" |
|
252 if @exclude |
|
253 and @$_ = grep { not -e } unique map { /^.*?:(.*)/ } @exclude; |
|
254 |
|
255 my @candidates = get_devices; |
|
256 my %missing; |
|
257 |
|
258 ### Candidates: @candidates |
|
259 foreach my $conf (@confs) { |
|
260 my @dles = _amlist $conf; |
|
261 ### DLEs: @dles |
|
262 |
|
263 foreach my $candidate (@candidates) { |
|
264 ### $candidate |
|
265 #next if not $candidate =~ m{^/|\Q$conf\E:}; |
|
266 |
|
267 # we're satisfied if either the name of the device is in |
|
268 # the disklist, or the device id is found |
|
269 $candidate->[0] ~~ [map { $_->[0] } @dles] and next; |
|
270 $candidate->[1] ~~ [map { $_->[1] } @dles] and next; |
|
271 push @{ $missing{$conf} }, $candidate->[0] |
|
272 if not "$conf:$candidate->[0]" ~~ @exclude; |
|
273 } |
|
274 } |
|
275 die map { "$_ missing: " . join(', ' => @{ $missing{$_} }) . "\n" } |
|
276 keys %missing |
|
277 if %missing; |
|
278 |
|
279 return map { $_->[0] } @candidates; |
|
280 } |
|
281 |
|
282 sub OK { say "$NAME OK\n", join "\n" => @_; exit 0 } |
|
283 sub WARNING { print "$NAME WARNING\n", join "\n" => @_; exit 1 } |
|
284 sub CRITICAL { print "$NAME CRITICAL\n", join "\n" => @_; exit 2 } |
|
285 sub UNKNOWN { print "$NAME UNKNOWN\n", join "\n" => @_; exit 3 } |
|
286 |
|
287 1; |
|
288 |
|
289 __END__ |
|
290 |
|
291 =pod |
|
292 |
|
293 =head1 NAME |
|
294 |
|
295 check_amanda-client - check the amanda backup from the client side |
|
296 |
|
297 =head1 SYNOPSIS |
|
298 |
|
299 check_amanda-client [-h|--help] [-m|--man] |
|
300 check_amanda-client [options] |
|
301 |
|
302 =head1 DESCRIPTION |
|
303 |
|
304 This nagios check plugin checks the Amanda setup from the client side. |
|
305 |
|
306 =head1 OPTIONS |
|
307 |
|
308 =over |
|
309 |
|
310 =item B<-x>|B<--exclude> [I<config:>]I<filesystem> |
|
311 |
|
312 The name of a filesystem to be excluded. |
|
313 No config means all configs. |
|
314 |
|
315 check_amanda-client --exclude weekly:/var/spool/squid --exclude /boot |
|
316 |
|
317 |
|
318 =item B<-h>|B<--help> |
|
319 |
|
320 Show a short description and help text. |
|
321 |
|
322 =item B<-m>|B<--man> |
|
323 |
|
324 Show the man page of this tool. |
|
325 |
|
326 =item B<-v>|B<--verbose> |
|
327 |
|
328 Show what's going only. Many for debugging purpose. (default: off) |
|
329 |
|
330 =back |
|
331 |
|
332 =head1 PREPARATIONS |
|
333 |
|
334 In order to make the check working, some preparations needs to be done. |
|
335 |
|
336 =head1 Client |
|
337 |
|
338 For each backup set you want to check: Create an |
|
339 F</etc/amanda/$set/amanda-client.conf>. |
|
340 |
|
341 config "foo" |
|
342 index-server "amanda.example.com" # used by restore |
|
343 tape-server "amanda.example.com" # used by restore |
|
344 amdump-server "amanda.example.com" # used by amdump_client |
|
345 |
|
346 In addition, the F<amservice> binary has to be suid root and executable |
|
347 by the backup user. This requirement is checked automatically by the |
|
348 plugin. |
|
349 |
|
350 =head1 Server |
|
351 |
|
352 The server need to know about the amdumpd service. In the F<inetd.conf> |
|
353 you need to add "amdumpd" to the list of allowed services. And |
|
354 additionally in F<.amandahosts> the "backup" user of the client needs |
|
355 the permissions to run the "amdumpd" service. |
|
356 |
|
357 # inetd.conf |
|
358 amanda stream tcp nowait backup /usr/lib/amanda/amandad amandad -auth=bsdtcp amindexd amidxtaped amdumpd |
|
359 |
|
360 # .amandahosts |
|
361 client.example.com backup amdumpd |
|
362 client.example.com root amindexd amidxtaped |
|
363 |
|
364 =head1 AUTHOR |
|
365 |
|
366 Heiko Schlittermann L<hs@schlittermann.de> |
|
367 |
|
368 =head1 SOURCE |
|
369 |
|
370 Source can be found at L<https://ssl.schlittermann.de/hg/nagios/nagios-plugin-amanda-client> |
|
371 |
|
372 =cut |
|
373 |
|
374 # vim:et ts=4 sw=4 aw ai: |
|