plugins/check_amanda-client
changeset 74 7d87261e4396
parent 73 112c0d86f3d4
equal deleted inserted replaced
73:112c0d86f3d4 74:7d87261e4396
     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: