# WardenWatchdog.pm
#
# Copyright (C) 2011-2014 Cesnet z.s.p.o
#
# Use of this source is governed by a BSD-style license, see LICENSE file.

package WardenWatchdog;

use strict;
use warnings;

use DBI;
use DBD::mysql;
use DateTime;
use Email::Simple;
use Sys::Hostname;

our $VERSION = "2.2";

#-------------------------------------------------------------------------------
# sendmailWrapper
#
# Simple wrapper function for an mailserver.
#
# Input:
#     message    = prepared message
#     email_conf = configuration of a mailserver
#
# Output: -
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub sendmailWrapper
{

  my $message    = shift;
  my $email_conf = shift;

  if(open(my $sendmail, $email_conf)) {
    print $sendmail $message;
    close $sendmail;
    return (1);
  } else {
    return (0, "Sending email failed: $!");
  }
}

# Array of hashes
#{query => ; text => ; contact => }


#-------------------------------------------------------------------------------
# sendReport
#
# Function for creating and sending of an Watchdog status report via email to
# administrators of an clients.
#
# Input:
#   Hash of parameters:
#     contact    => email address of a message recipient
#     domain     => domain name of server where script runs
#     subject    => subject of an email
#     text       => body of an email
#     email_conf => configuration of a mailserver
#
# Output: -
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub sendReport
{

  my $input_data = shift;
  my $contact    = $$input_data{'contact'};
  my $domain     = $$input_data{'domain'};
  my $subject    = $$input_data{'subject'};
  my $text       = $$input_data{'text'};
  my $email_conf = $$input_data{'email_conf'};

  my $message;
  if(!($contact)) {
    return (0, "Empty 'To' email header!\n");
  }

  if(!($domain)) {
    return (0, "No sender's domain! Can't send email\n");
  }

  if(!($text)) {
    return (0, "No text! Nothing to send\n");
  }

  eval{
    $message = Email::Simple->create(
    header => [
      To      => $contact,
      From    => 'warden_watchdog@'.$domain,
      Subject => $subject],
    body   => $text);
  } or do {
    return (0, "Can't create email message\n");
  };

  my ($rc, $err) = sendmailWrapper($message->as_string,$email_conf);
  if(!$rc) {
    return (0, $err);
  }
  return (1);
}

#-------------------------------------------------------------------------------
# connectToDB
#
# Just simple database wrapper for Watchdog which creates db's handler.
#
# Input:
#   Hash of parameters:
#     platform => database platform name according to Perl DBI
#     name     => name of a database
#     hostname => database hostname
#     user     => username
#     passwd   => password
#
# Output:
#   dbh_ref = reference on a database handler
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub connectToDB
{

  my $db_conf = shift;
  my $dbh_ref = shift;

  my $db_platform = $$db_conf{'platform'};
  my $db_name     = $$db_conf{'name'};
  my $db_hostname = $$db_conf{'hostname'};
  my $db_user     = $$db_conf{'user'};
  my $db_passwd   = $$db_conf{'passwd'};

  my $dbh;
  if($dbh = DBI->connect( "dbi:$db_platform:database=$db_name;host=$db_hostname", $db_user, $db_passwd, {mysql_auto_reconnect => 1})) {
    $$dbh_ref = $dbh;
    return (1);
  }
  else{
    return (0,"Cannot connect to database! ".DBI->errstr);
  }
}

#-------------------------------------------------------------------------------
# updateProcedures
#
# Function takes DB handler and executes all database procedures in the array.
#
# Input:
#   dbh_ref  = reference on a database handler
#   proc_ref = reference on an array of database procedures
#
# Output: -
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub updateProcedures
{

  my $dbh_ref  = shift;
  my $proc_ref = shift;

  my $dbh = $$dbh_ref;
  if(!defined($dbh)) {
    return (0, "updateProcedures: no database handle defined")
  }

  my @sql_queries = @{$proc_ref};
  foreach my $proc (@sql_queries) {
    $dbh->do($proc);
  }

  return (1);
}

#-------------------------------------------------------------------------------
# sendQuery
#
#
#
# Input:
#   dbh_ref    = reference on a database handler
#   config_ref = Hash of parameters:
#     query   => sql query of an action (check) on Warden database
#     text    => body of an email which is send to a admin of an client
#                in case of nonempty check result
#     contact => contact for message, which overrides contact collumn
#                in a database table.
#
# Output:
#   events_ref = Hash of parameters:
#     contact   = email address of an client administrator
#     'contact' => predefined email text + information from database obtained
#                  by a query
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub sendQuery
{

  my $dbh_ref    = shift;
  my $config_ref = shift;
  my $events_ref = shift;

  my $dbh = $$dbh_ref;
  if(!defined($dbh)) {
    return (0, "sendQuery: no database handle defined")
  }

  my @config = @{$config_ref};
  my %bad_events;
  my ($rc,$err);
  my $i = 0;

  while ($i < scalar(@config)) {
    # run DB query -> requestor, client name
    my $sth;
    if (defined($config[$i]{query})) {
      $sth = $dbh->prepare($config[$i]{query});
    }
    else{
      return (0, "No query available\n");
    }

    if (!($sth->execute)) {
      return (0, "Couldn't get data from my database: $sth->errstr\n");
    };

    my $contact;
    my $msg_text = 1;

    while(my $result = $sth->fetchrow_hashref()) {
      if (defined($config[$i]{contact})) {
        # override contact from 'requestor' collumn
        $contact = $config[$i]{contact};
      }
      else{
        $contact = $result->{'requestor'};
      }
      # information header
      if($msg_text) {
        $bad_events{$contact} .= $config[$i]{text} . "\n\n";
        $bad_events{$contact} .= join(" | ", map {$_ // "UNKNOWN" } keys %$result) . "\n";
        $msg_text = 0;
      }
      $bad_events{$contact} .= join(" | ", map {$_ // "NULL" } values %$result) . "\n";
    }
    foreach my $key (keys %bad_events) {
      $bad_events{$key} .= "\n\n";
    }

    $sth->finish;
    $i++;
  }

  %$events_ref = %bad_events;
  return (1);
}

#-------------------------------------------------------------------------------
# run
#
# Main module function which takes configure file, interval and performs Warden
# database checks described in a configuration file. Results are send to client
# administrators or on a defined addresses if they are set in a config.
#
# Input:
#   conf_file = path to a config file with database procedures and other params
#   period    = interval in days from now back to the past for database check
#
# Output: -
#
# Return:
#   On Success (1)
#   On Failure (0, 'Error message')
#-------------------------------------------------------------------------------
sub run
{

  my $conf_file = shift;
  my $period    = shift;

  my $err_msg;
  # server config

  if(!defined($conf_file)) {
    return (0,"No conf file is available");
  }

  if(!defined($period)) {
    return (0,"No time period is defined");
  }

  our $SERVER_CONF       = undef;
  our $DOMAIN_NAME       = undef;
  our $EMAIL_SUBJECT     = undef;
  our $EMAIL_SERVER_CONF = undef;
  our @SQL_PRECONDITION  = undef;
  our @SQL_QUERIES       = undef;
  our @SQL_POSTCONDITION = undef;

  # script config
  if (!(do $conf_file)) {
    if ($@) {
      $err_msg = "Errors in config file '$conf_file': $@";
      #syslog("err|$err_msg");
      print $err_msg;
      return (0,"Warden watchdog - $err_msg");
    }
    if (!(defined $_)) {
      $err_msg = "Can't read config file '$conf_file': $!";
      #syslog("err|$err_msg");
      print $err_msg;
      return (0,"Warden watchdog - $err_msg");
    }
  }

  # server config
  our $SYSLOG_VERBOSE  = undef;
  our $SYSLOG_FACILITY = undef;
  our $DB_NAME         = undef;
  our $DB_USER         = undef;
  our $DB_PASS         = undef;
  our $DB_HOST         = undef;

  # TODO replace with function call from Wardencommon
  if (!(do $SERVER_CONF)) {
    if ($@) {
      $err_msg = "Errors in config file '$SERVER_CONF': $@";
      #syslog("err|$err_msg");
      print $err_msg;
      return (0,"Warden watchdog - $err_msg");
    }
    if (!(defined $_)) {
      $err_msg = "Can't read config file '$SERVER_CONF': $!";
      #syslog("err|$err_msg");
      print $err_msg;
      return (0,"Warden watchdog - $err_msg");
    }
  }

  #print "SYSLOG_VERBOSE $SYSLOG_VERBOSE, SYSLOG_FACILITY $SYSLOG_FACILITY, DB_NAME $DB_NAME, DB_USER $DB_USER, DB_PASS $DB_PASS, DB_HOST $DB_HOST";
  my %db_conf = (platform => 'mysql', name => $DB_NAME, hostname => $DB_HOST, user => $DB_USER, passwd => $DB_PASS);

  my $date;
  eval{
    my $dt = DateTime->now(time_zone => 'UTC')->subtract(days => $period);
    $dt->set_time_zone('local');
    $date = $dt->strftime("%Y-%m-%d %H:%M:%S");
  } or do {
    #print "Warden watchdog - can't work with date\n";
    syslog("Warden watchdog - can't work with date\n");
  };



  my $dbh;
  # connect to DB
  my ($rc,$err) = connectToDB(\%db_conf,\$dbh);
  if (!$rc) {
    $err_msg = "Warden watchdog can\'t connect do DB: $err";
    return (0,"Warden watchdog - $err_msg");
  }

  if(@SQL_PRECONDITION) {
    ($rc,$err) = updateProcedures(\$dbh,\@SQL_PRECONDITION);
    if (!$rc) {
      #print "Warden watchdog - $err\n";
      return (0,"Warden watchdog - $err\n");
    }
  }

  my %bad_events;
  if(@SQL_QUERIES) {
    foreach my $query (@SQL_QUERIES) {
      $query->{query} =~ s/\$date/$date/;
    }
    my ($rc,$err) = sendQuery(\$dbh,\@SQL_QUERIES,\%bad_events);
    if (!$rc) {
      #print "Warden watchdog - $err\n";
      return (0,"Warden watchdog - $err\n");
    }
  }

  if(@SQL_POSTCONDITION) {
    my ($rc,$err) = updateProcedures(\$dbh,\@SQL_POSTCONDITION);
    if (!$rc) {
      #print "Warden watchdog - $err\n";
      return (0,"Warden watchdog - $err\n");
    }
  }

  while (my ($contact, $text) = each(%bad_events)) {
    my %input = (contact => $contact, domain => $DOMAIN_NAME, text => $text, subject => $EMAIL_SUBJECT, email_conf => $EMAIL_SERVER_CONF);
    my ($rc,$err) = sendReport(\%input);
    if (!$rc) {
      #print $err;
      return (0,"Warden client - networkReporter $err\n");
    }
  }

  # disconnect to DB
  $dbh->disconnect;
}
1;