|      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: |         |