#!/usr/bin/perl -w
#
# Bacula(R) - The Network Backup Solution
#
# Copyright (C) 2013-2013 Bacula Systems(R) SA
#
# Bacula(R) is a registered trademark of Kern Sibbald.
# Bacula Systems(R) is a trademark of Bacula Systems SA.
# Bacula Enterprise(TM) is a trademark of Bacula Systems SA.
#
# The licensor of Bacula Enterprise(TM) is Bacula Systems(R) SA,
# Rue Galilee 5, 1400 Yverdon-les-Bains, Switzerland.
#
#

use strict;
use File::Path;
use Getopt::Long;
use IPC::Open2;
use POSIX 'strftime';
use Data::Dumper;
use File::Temp qw/tempfile/;

$ENV{PATH}='/opt/bacula/bin:' . $ENV{PATH};

my $VERSION="0.2";
my $HEADER="Bacula Enterprise Pro-Active Monitoring Tool $VERSION";


my $debug     = 0;
my $testing   = 0;
my $email     = 'bsalert@baculasystems.com';
my $conf      = '/opt/bacula/etc/bs_alert.conf';
my $bconsole  = 'bconsole -u 10';
my $url       = 'https://www.baculasystems.com/bs_alert/';
my $all       = {};
my $yesterday = time() - 24*60*60;
my $lastweek  = time() - 7*24*60*60;
my $overwrite = {};             # Read bs_alert.conf before
my $bymail    = 0;
my $byhttps   = 0;
my $fh;
my $filename;

GetOptions ("d|debug"         => \$debug,
            "testing"         => \$testing,
            "to=s"            => \$email,
            "mail"            => \$bymail,
            "https"           => \$byhttps,
            "conf=s"          => \$conf,
            "b|bconsole=s"    => \$bconsole,
            "h|help"          => \&HELP_MESSAGE) || HELP_MESSAGE();


if ($bymail || $byhttps) {
    ($fh, $filename) = File::Temp::tempfile();
    select $fh;

    #close(STDERR);
    #open(STDERR, ">/dev/null");
}


sub header
{
    print "\n###############################################################\n";
    print "# @_\n";
    print "###############################################################\n\n";
}

sub HELP_MESSAGE
{
    print $HEADER, "\n";
    print "Copyright Bacula Systems(R) 2013\n";
    exit 1;
}

sub load_config
{
    my ($config) = @_;
    if (open(FP, $config)) {
 
        # W  label  operator
        while (my $l = <FP>) {
            next if ($l =~ /^($|\s+|#)/);
            if ($l =~ /^(\w+)\s+([\w\d\-\.]+)\s+(.*)/) {
                $overwrite->{$2}->{op}    = $3;
                $overwrite->{$2}->{level} = $1;
            }
        }

        close(FP);
    }
}

sub check_values
{
    my ($settings, $all) = @_;
    my $header;
    # parse conf 
    foreach my $l (split('\n', $settings)) {
    
        next if ($l eq "");

        # #  HEADER
        if ($l =~ /#\s+(.+)/) {
            $header = $1;
            next;
        }
            
        # W  label  operator
        if ($l !~ /(\w+)\s+([\d\.\-\w]+)\s+(.*)$/) {
            next;
        }

        my ($level, $key, $op) = ($1, $2, $3);
        
        # Check if we overwrite this setting
        if ($overwrite->{$key}) {
            $level = $overwrite->{$key}->{level};
            $op    = $overwrite->{$key}->{op};
        }
        
        # not in settings
        next if (not defined $all->{$key});

        my $res = -1;
        eval("\$res = ( \$all->{\$key} $op )");
        
        if ($res == 1 || $debug) {
            if ($header) {
                header($header);
                undef $header;
            }
        }

        if ($res == 1) {
            print "$level  $key  $op  ($all->{$key})\n";
            # report problem

        } elsif ($debug) {
            print "OK $key  $op  ($all->{$key})\n";
        }
    }
}

# If we have a configuration file, use it
if (-f $conf) {
    load_config($conf);
}


# Start by giving some information about the system
header($HEADER);

$all->{Date} = strftime('%Y-%m-%d %H:%M:%S %Z', localtime(time));
$all->{Hostname} = `hostname`;
$all->{Version} = `echo version | $bconsole | tail -1`;
$all->{bsalert_version} = $VERSION;

foreach my $k (sort keys %$all) {
    chomp($all->{$k});
    print $k, ": ", $all->{$k}, "\n";
}

################################################################
# Get memory and swap
my @mem = `free -k`; 

# Mem:  total  used  free  shared ...
# Swap: total  used  free
for my $l (@mem) {
    if ($l !~ /(Mem|Swap):\s*(\d+)/) {
        next;
    }
    $all->{$1} = $2;
}

################################################################
# Check disk usage
sub check_disk
{
    my @out = `df -k -P`;

    my $opt_R = q/^$/;          # Exclude
    my $opt_r = "";             # Include
    my $exclude_re = "(^//|^none|^tmpfs|^dev|^run)";


    my %alldisks;
#
#/dev/hda1               459143     68879    365767  16% /
#tmpfs                   128056         0    128056   0% /dev/shm
#/dev/mapper/vgroot-home
#                       2097084    202824   1894260  10% /home
#/dev/mapper/vgroot-usr
#                       2097084    911280   1185804  44% /usr

    foreach my $l (@out) {
        if($l =~ /(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\%\s+([\/\w\d\.\-\s]+)$/) {
            my ($s,$u,$f,$pu,$d) = ($1,$2,$3,$4,$5);
            next if ($l =~ m/$opt_R/);
            next if ($l !~ m/$opt_r/);
            next if ($l =~ m/$exclude_re/);

            $alldisks{$d}->{pused} = $pu;
            $alldisks{$d}->{pfree} = 100-$pu;
            $alldisks{$d}->{somme} = $s*1024;
            $alldisks{$d}->{used} = $u*1024;
            $alldisks{$d}->{free} = $f*1024;

# on aix, we two other fileds for inodes
#Filesystem    1024-blocks      Free %Used    Iused %Iused Mounted on
#/dev/hd4           262144    186880   29%     5119    11% /
            
        } elsif ($l =~ /(\d+)\s+(\d+)\s+(\d+)\%\s+(?:\d+\s+\d+\%)\s+([\/\w\d\.-]+)$/) {
            next if ($l =~ m/$opt_R/);
            next if ($l !~ m/$opt_r/);
            next if ($l =~ m/$exclude_re/);
            my ($s,$f,$pu,$d) = ($1,$2,$3,$4);
            $alldisks{$d}->{pused} = $pu;
            $alldisks{$d}->{pfree} = 100-$pu;
            $alldisks{$d}->{somme} = $s*1024;
            $alldisks{$d}->{used} = ($s-$f)*1024;
            $alldisks{$d}->{free} = $f*1024;
        }
    }
}

################################################################
# Look for traceback files generated during the last 24 hours
my @tracebacks = grep { -M $_ <= 1 } </opt/bacula/working/*traceback>;
$all->{nb_traceback} = scalar(@tracebacks);

################################################################
# Look for sysctl
if ($^O eq "linux") {
    if (open(FP, "sysctl -a 2>/dev/null|")) {
        while (my $l = <FP>) {
            if ($l !~ /([\w\d\-\.]+)\s+=\s+(.*)$/) {
                next;
            }
            $all->{$1} = $2;
        }
        close(FP);
       
    } else {
        print STDERR "ERROR: Unable to run sysctl $@\n";
    }
}

################################################################
# Query Bacula
my $pid = open2(\*CHLD_OUT, \*CHLD_IN, $bconsole);

if ($pid > 0) {
    print CHLD_IN "use\n1\ngui on\nsql\n",
    "SELECT version();\n",
    "SELECT name, setting, unit, short_desc FROM pg_settings;\n",
    "SHOW VARIABLES;\n",
    "SELECT JobStatus, count(1) AS nb FROM Job WHERE JobTDate > $yesterday GROUP BY JobStatus;\n",
    "SELECT 'AllJobs', count(1) AS nb FROM Job WHERE JobTDate > $yesterday;\n",
    "SELECT 'Job.LongJobs', count(1) AS nb FROM Job WHERE JobTDate > $lastweek AND date_part('sec', (EndTime - StartTime)) > 8*24*60*60;\n",
    "SELECT 'Job.LongJobs', count(1) AS nb FROM Job WHERE JobTDate > $lastweek AND (EndTime - StartTime) > 8*24*60*60;\n",
    "SELECT 'Clients', Count(1) FROM Client;\n",
    "SELECT 'Job.Logs', Count(1) AS nb FROM Log GROUP BY JobId ORDERY BY nb DESC LIMIT 5;\n",
    "SELECT 'Media.MaxVolBytes', MAX(VolBytes) FROM Media;\n",
    "SELECT 'Full.Job.Without.Files',  Count(1) AS nb FROM Job WHERE Type='B' AND Level='F' AND JobStatus = 'T' AND JobFiles=0 AND JobTDate > $lastweek;\n",
    "\nquit\n";
    close(CHLD_IN);
    while (my $l = <CHLD_OUT>) {
        if ($l =~ /PostgreSQL (\d+)\.(\d+)\.\d+/) {
            $all->{pg_version} = $1*100 + $2;
            next;
        }
        if ($l !~ /([\w\d\.\-]+)\s+\|\s+([\w\d\-\.]+)\s+\|/) {
            next;
        }
        $all->{$1} = $2;
    }
    close(CHLD_OUT);
    waitpid($pid, 0);

} else {
    print STDERR "ERROR: Unable to start bconsole ($bconsole) $@\n";
}

if ($all->{pg_version}) {
    my $v = `pg_config --version`;
    if ($v && $v =~ /PostgreSQL (\d+)\.(\d+)\.\d+/) {
        $all->{pq_version} = $1 * 100 + $2;
        if ($all->{pq_version} < 900 && $all->{pg_version} >= 900) {
            $all->{libpq} = "Not compatible with Postgres server";
        } else {
            $all->{libpq} = "OK";
        }
    }
}

################################################################
# Compute some extra information
if ($all->{AllJobs} && $all->{T}) {
    $all->{"Job.OK.percent"} = sprintf('%.2f', $all->{T} / $all->{AllJobs} * 100);
}

if ($all->{AllJobs} && $all->{f}) {
    $all->{"Job.Failed.percent"} = sprintf('%.2f', $all->{f} / $all->{AllJobs} * 100);
}

my $settings_check = "
#  Basic System Configuration
W  Mem       < 1048576
W  Swap      < 524288
f  Swap     == 0

#  System Configuration
W  vm.dirty_background_ratio  >  2
W  vm.dirty_background_ratio ==  0
W  vm.dirty_ratio             <  2
W  vm.swappiness             != 10
W  vm.zone_reclaim_mode      !=  0

#  Catalog Configuration
W  stats_start_collector ne 'on'
W  enable_hashagg        ne 'on'
W  enable_hashjoin       ne 'on'
W  enable_indexscan      ne 'on'
W  enable_mergejoin      ne 'on'
W  enable_nestloop       ne 'on'
W  enable_seqscan        ne 'on'
W  enable_sort           ne 'on'
W  enable_tidscan        ne 'on'
W  max_fsm_pages          < 10000
W  log_executor_stats    ne 'off'
W  log_parser_stats      ne 'off'
W  log_planner_stats     ne 'off'
W  log_statement_stats   ne 'off'
W  log_checkpoints       ne 'on'
W  random_page_cost      != 4
W  autovacuum            ne 'on'
W  wal_buffer             < 4194304
W  wal_buffer             > 8388608
W  checkpoint_segments    < 16
W  checkpoint_segments    > 512
W  checkpoint_timeout     < 1200
W  effective_cache_size   < 131072
W  work_mem               < 32768
W  work_mem               > 1048576
W  maintenance_work_mem   < 102400
W  shared_buffers         < 131072
W  shared_buffers         > 1310720
f  fsync                 ne 'on'
f  libpq                 ne 'OK'

W  innodb_buffer_pool_size  < 104857600

#  Job Statistics
W  T     ==  0
W  f     >=  300
W  Job.OK.percent     < 60
W  Job.Failed.percent > 30
W  Job.LongJobs       > 10
W  Job.Logs           > 10000
f  Job.Logs           > 100000
W  Media.MaxVolBytes  > 4398046511104
W  nb_traceback       > 0
W  Full.Job.Without.Files > 0
";

check_values($settings_check, $all);

if ($fh) {
    close($fh);
}

if ($bymail) {
    if (system("mail -s '$HEADER' $email < $filename") != 0) {
        # do something?
        print STDERR "Error when sending email...\n";
    }
}

if ($byhttps) {
    if (system("curl -q -F filedata=\@$filename $url") != 0 || $? != 0) {
        eval {
            require HTTP::Request::Common;
            require LWP::UserAgent;
            my $userAgent = LWP::UserAgent->new();
            my $request = POST($url, 
                               Content_Type => 'multipart/form-data', 
                               Content => [filedata => [$filename]]);

            my $response = $userAgent->request($request);
            if ($response->is_error) {
                print STDERR "Error when uploading result ", $response->status_line, "...\n";
            }
        }
    }
}

if ($filename) {
    unlink($filename);
}
