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