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