#! /usr/bin/perl
# source: https://ssl.schlittermann.de/hg/ius/nagios/nagios-plugin-amanda-client

use 5.010;
use strict;
use warnings;
use Getopt::Long;
use POSIX;
use File::Spec::Functions;
use Data::Dumper;
use File::Find;
use Carp;
use Pod::Usage;

our $VERSION = '0.0.2';

sub su;
sub find_tool;
sub check_perms;
sub config_names;
sub get_devices;

sub amchecks;
sub compare_lists;

sub main;

my $NAME = 'AMANDA-CLIENT';

sub ok;
sub warning;
sub critical;
sub unknown;
sub verbose;
sub unique { my %h; @h{@_} = (); keys %h }

$SIG{__DIE__} = sub { unknown @_ unless $^S };

exit main @ARGV;

#----

sub main {
    my @opt_ignore;
    my $opt_verbose = 0;

    GetOptions(
        'i|ignore=s@' => \@opt_ignore,
        'h|help'      => sub { pod2usage(-verbose => 1, -exit => 0) },
        'm|man'       => sub { pod2usage(-verbose => 2, -exit => 0) },
        'v|verbose'   => \$opt_verbose,
    ) or pod2usage;

    if ($opt_verbose) {
        *::verbose = sub { say '# ', @_ }
    }
    else {
        *::verbose = sub { }
    }

    # test needs to be run as root:* or as backup:backup
    my $USER  = 'backup';
    my $CFDIR = '/etc/amanda';

    # change to backup if still root
    su $USER if $> == 0;

    # amservice needs to be suid root, but executable
    # by the backup user/group
    verbose q{checking permissions for `amservice'};
    eval { check_perms find_tool('amservice'), 04750, 'root', $) }
      or unknown $@;

    # find the backup sets we know about
    # here we suppose that it's possible to find strings like
    # 'conf "foo"' in files named 'amanda-client.conf' below /etc/amanda

    verbose qq{find config names from $CFDIR};
    my @confs = sort +unique eval { config_names $CFDIR }
      or unknown $@;

    eval { amchecks @confs } or critical $@;

    my @dles = eval { compare_lists confs => \@confs, ignore => \@opt_ignore } or critical $@;
    ok 'config: ' . join(', ', @confs), @dles;

    # never reached
    return 0;
}

# compare the file systems
# get a list of file system
sub get_devices {
    open(my $fh, '/proc/filesystems');
    my @types = map { /^\s+(\S+)/ ? $1 : () } <$fh>;
    my @df = (df => '-P', map { -t => $_ } @types);
    map { [$_, (stat)[0]] } map { (split ' ', $_)[5] } grep { /^\// } `@df`;
}

sub su {
    my $user  = shift;
    my $group = (getgrnam $user)[0];
    my $uid   = getpwnam $user;
    my $gid   = getgrnam $group;

    my @groups;

    setgrent;
    my @rc;
    while (my @g = getgrent) {
        push @groups, $g[2] if $user ~~ [split ' ', $g[3]];
    }
    endgrent;
    $) = "@groups";

    verbose "su to $uid:$gid";
    setgid $gid;
    setuid $uid;
}

sub find_tool {
    my $name = shift;
    my @rc = grep { -f -x } map { catfile $_, $name } split /:/, $ENV{PATH}
      or die "Can't find `$name' in $ENV{PATH}\n";
    $rc[0];
}

sub check_perms {
    my ($file, $mode, $owner, $group) = @_;

    $owner = getpwuid $owner if $owner ~~ /^\d+$/;

    $group = getgrgid +(split ' ', $group)[0]
      if $group ~~ /^[\d\s]+$/;

    stat $file or croak "Can't stat `$file': $!\n";

    eval {
        my $f_owner = getpwuid +(stat _)[4] or die $!;
        my $f_group = getgrgid +(stat _)[5] or die $!;
        my $f_mode  = (stat _)[2] & 07777   or die $!;

        my $msg =
          sprintf "need: 0%04o root:$group, got: 0%04o $f_owner:$f_group\n",
          $mode, $f_mode;

        die $msg unless $f_owner eq $owner;
        die $msg unless $f_group eq $group;
        die $msg unless $f_mode == $mode;
    };
    die "wrong permissions for `$file', $@" if $@;
    1;
}

sub config_names {
    my $dir     = shift;
    my @configs = ();
    find(
        sub {
            -f and /^amanda-client\.conf$/ or return;
            open(my $fh, '<', $_) or die "Can't open  $File::Find::name: $!\n";
            push @configs, map { /^conf\s+"(.+?)"/ ? $1 : () } <$fh>;
        },
        $dir
    );

    die
"no configs found below $dir (amanda-client.conf needs need `conf \"foo\"' line)\n"
      if not @configs;
    return @configs;
}

sub _amcheck {

    #config: daily
    #CHECKING
    #
    #Amanda Backup Client Hosts Check
    #--------------------------------
    #Client check: 1 host checked in 2.242 seconds.  0 problems found.
    #
    #(brought to you by Amanda 3.3.1)
    #The check is finished

    my $conf = shift;
    my $_    = qx(amdump_client --config '$conf' check 2>&1);
    /^config:\s+$conf\n
		 CHECKING\n
		 .*\n
		 Client.check:.1.host.checked.in.\d+\.\d+.seconds\.\s+0.problems.found\.\n
		 .*\n
		 The.check.is.finished$
	/smx or die "unexpected output from check:\n$_";
}

sub amchecks {
    my @errors = ();
    foreach my $conf (@_) {
        eval { _amcheck $conf } or push @errors, $@;
    }
    die @errors if @errors;
    return 1;
}

sub _amlist {

    # return a list of [ name, dev ] tupels.
    # name: the name of the disk/device
    # dev:  the local device id (stat)[0]
    # iff the inum of $name != 2, it's not the top directory
    # and we set the device id to -1, since $name does not stand for a whole
    # device
    my $conf = shift;
    chomp((undef, my @dles) = qx(amdump_client --config '$conf' list));
    return map { [$_, (stat $_)[1] == 2 ? (stat $_)[0] : -1] } @dles;
}

sub compare_lists {
    my %arg = @_;
    my @confs = @{$arg{confs}} or croak 'missing list of confs';
    my @ignore = @{$arg{ignore}};

    warning "ignored filesystem(s) @$_ does not exist, update the config please!\n"
	if @ignore and @$_ = grep { not -e } unique map { /^.*?:(.*)/ } @ignore;

    my @candidates = get_devices;
    my %missing;

    foreach my $conf (@confs) {
        my @dles = _amlist $conf;
        foreach my $candidate (@candidates) {

            # we're satisfied if either the name of the device is in
            # the disklist, or the device id is found
            $candidate->[0] ~~ [map { $_->[0] } @dles] and next;
            $candidate->[1] ~~ [map { $_->[1] } @dles] and next;
            push @{ $missing{$conf} }, $candidate->[0]
                if not "$conf:$candidate->[0]" ~~ @ignore;
        }
    }
    die map { "$_ missing: " . join(', ' => @{ $missing{$_} }) . "\n" }
      keys %missing
      if %missing;

    return map { $_->[0] } @candidates;
}

sub ok { say "$NAME OK\n", join "\n" => @_; exit 0 }
sub warning  { print "$NAME WARNING\n",  join "\n" => @_; exit 1 }
sub critical { print "$NAME CRITICAL\n", join "\n" => @_; exit 2 }
sub unknown  { print "$NAME UNKNOWN\n",  join "\n" => @_; exit 3 }

__END__

=pod

=head1 NAME

 check_amanda-client - check the amanda backup from the client side

=head1 SYNOPSIS

  check_amanda-client [-h|--help] [-m|--man]
  check_amanda-client [options]

=head1 DESCRIPTION

This nagios check plugin checks the Amanda setup from the client side.

=head1 OPTIONS

=over

=item B<-i>|B<--ignore> I<config:filesystem>

The name of a filesystem to be ignored. Example:

    check_amanda-client --ignore weekly:/var/spool/squid --ignore daily:/boot

=item B<-h>|B<--help>

Show a short description and help text.

=item B<-m>|B<--man>

Show the man page of this tool.

=item B<-v>|B<--verbose>

Show what's going only. Many for debugging purpose. (default: off)

=back

=head1 PREPARATIONS

In order to make the check working, some preparations needs to be done.

=head1 Client

For each backup set you want to check: Create an
F</etc/amanda/$set/amanda-client.conf>. 

    config "foo"
    index-server    "amanda.example.com"    # used by restore
    tape-server	    "amanda.example.com"    # used by restore
    amdump-server   "amanda.example.com"    # used by amdump_client

In addition, the F<amservice> binary has to be suid root and executable
by the backup user. This requirement is checked automatically by the
plugin.

=head1 Server

The server need to know about the amdumpd service. In the F<inetd.conf> 
you need to add "amdumpd" to the list of allowed services. And
additionally in F<.amandahosts> the "backup" user of the client needs
the permissions to run the "amdumpd" service.

    # inetd.conf
    amanda stream tcp nowait backup /usr/lib/amanda/amandad amandad -auth=bsdtcp amindexd amidxtaped amdumpd

    # .amandahosts
    client.example.com backup amdumpd
    client.example.com root amindexd amidxtaped 

=head1 AUTHOR

Heiko Schlittermann L<hs@schlittermann.de>

=head1 SOURCE

Source can be found at L<https://ssl.schlittermann.de/hg/nagios/nagios-plugin-amanda-client>

=cut

# vim:et ts=4 sw=4 aw ai:
