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