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