#!/usr/bin/perl
# notifier -- launches new sends

use strict;
use Getopt::Long;
use Net::SMTP;
use NOCpulse::Config;
use NOCpulse::Log::Logger;
use NOCpulse::Notif::AlertFile;
use NOCpulse::Notif::AlertDB;
use NOCpulse::Notif::EscalatorInterface;
use NOCpulse::Notif::RotateFirstEscalateStrategy;

umask(002);

my $program = 'notifier';
my $CONFIG  = new NOCpulse::Config;

# Get command line options
my $logopt = {};
GetOptions('log=s%' => $logopt);

#Set up for graceful exit
my $bailout = 0;
$SIG{'INT'} = $SIG{'TERM'} = sub { $bailout = 1; };

# Setup logging
my $LOGDIR     = $CONFIG->get('notification', 'log_dir');
my $verboselog = "$LOGDIR/$program.log";
my $LOG_CONFIG = "( '$program'                              => 3, 
                            'NOCpulse::Notif::Redirect'             => 1, 
                            'NOCpulse::Notif::Alert'                => 1, 
                            'NOCpulse::Notif::FileQueue'            => 1 )";

my ($Log, $stream) = &setup_logging();

my $lastlog = 0;

# Temporary directory for in progress information
my $TMPDIR = $CONFIG->get('notification', 'tmp_dir');
my $SOCKET_FILENAME =
  "$TMPDIR/escsock";    ## TBD -- replace with config file entry

# Get the config information
my $cfg      = NOCpulse::Config->new;
my $cfg_file =
  $CONFIG->get('notification', 'config_dir')
  . '/static/notif.ini';
my $notify_cfg =
  new Config::IniFiles(-file   => $cfg_file,
                       -nocase => 1);

my $SERVER_ID =
  $notify_cfg->val('server', 'serverid')
  ;    # $server_recid is the recid of the notification server
my $SERVER_IP = $notify_cfg->val('server', 'serverip');

my $MX         = $CONFIG->get('mail',         'mx');
my $MAILDOMAIN = $CONFIG->get('mail',         'maildomain');
my $REPLYBASE  = $CONFIG->get('notification', 'frombase');
my $REPLYNAME  = $CONFIG->get('notification', 'fromname');
my $REPLYADDR = sprintf("\"%s\" <%s%02d\@%s>",
                        $REPLYNAME, $REPLYBASE, $SERVER_ID, $MAILDOMAIN);

# Connect to the smtp server
my $TIMEOUT = 30;
my $smtp;

# Collection of backlog information for the escalator
my @backlog;

# Connect to the database
my $db = NOCpulse::Notif::AlertDB->new;
$db->connect;

# Daemon loop
$Log->log(1, "Starting daemon loop\n");

my @files = NOCpulse::Notif::AlertFile->remove_locks($TMPDIR, $program);
if (scalar(@files)) {
  $Log->log(3, "Removed stale locks\n");
  $Log->log(9, "@files\n");
}

# Main loop
while (1) {

  # If a shutdown was requested, exit gracefully
  if ($bailout) {
    $Log->log(1, "System shutdown requested\n");
    my @files = NOCpulse::Notif::AlertFile->remove_locks($TMPDIR, $program, $$);
    if (scalar(@files)) {
      $Log->log(3, "Removed stale locks\n");
      $Log->log(9, "@files\n");
    }
    $smtp->quit()     if $smtp;
    $db->disconnect() if $db;
    $Log->log(1, "System shutdown completed\n");
    exit(0);
  } ## end if ($bailout)

  # Check the connection to the MX, and reconnect if necessary
  $smtp = &check_smtp_connection($smtp);

  # Get some more work
  my ($alert_filename, @send_ids);

  eval {
    my $escalator_if =
      NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
    ($alert_filename, @send_ids) = $escalator_if->next_sends;
  };

  if ($@) {
    $Log->log(1, "ERROR: EscalatorInterface next_sends error: $@\n");
  }

  if ($alert_filename) {

    # There's work to be done

    # Lock the alert file
    my $count      = 1;
    my $alert_file = &lock_alert_file($alert_filename);
    unless ($alert_file) {
      $Log->log(1,
             "ERROR: Unable to read alert from $alert_filename ... dropping\n");
      next;
    }

    my $alert = $alert_file->alert;
    unless ($alert) {
      $alert_file->delete;
      $Log->log(1, "ERROR: Alert not found from $alert_file ... dropping\n");
      next;
    }

    foreach my $send_id (@send_ids) {
      my $send = $alert->send_named($send_id);
      unless ($send) {
        $Log->log(
              "ERROR: Send $send_id not found from $alert_file ... dropping\n");
        next;
      }

      # Launch the send
      $Log->log(1, "Started ", $send->printString, "\n");

      #There must be a more elegant way (maybe another object here):
      my $dest = $send->destination;
      my $error;

      # if we are sending we need to reconnect to our smtp server to verify
      # that it is still available.
      $smtp = undef;
      $smtp = &check_smtp_connection($smtp);
      eval { $error = $dest->send($send, $alert, $db, $smtp); };
      if ($@) {
        $error .= $@;
      }
      
      if ($error) {
        $Log->log(1, "Send error ", $send->printString, ": $@ (nak) smtp code: $error\n");
        eval {
          my $escalator_if =
            NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
          $escalator_if->ack('nak', $send_id);
        };
        if ($@) {
          my @array = (1, 'ack', 'nak', $send_id);
          push(@backlog, \@array);
          $Log->log(1, "ERROR: EscalatorInterface ack error: $@\n");
        }
        next;
      } else {
        $Log->log(1, "Sent ", $send->printString, "\n");
        $send->send_time(time());
      }

      # Update the escalator
      eval {
        my $escalator_if =
          NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
        $escalator_if->update_send($send);
      };
      if ($@) {
        my @array = (1, 'update_send', $send);
        push(@backlog, \@array);
        $Log->log(1, "ERROR: EscalatorInterface update_send error: $@\n");
      }

    }    # end foreach $sendid

    # Save the send to disk so others may see
    $alert_file->close_file;

  } else {

    # Nothing to do

    if (time() - $lastlog >= 60) {

      #time to gripe
      $Log->log(1, "waiting for new sends to launch...\n");
      $lastlog = time();

      #process the backlog
      if (@backlog) {
        $Log->log(1, "processing escalator backlog\n");
        @backlog = &process_backlog(@backlog);
      }

    } ## end if (time() - $lastlog ...
    sleep(1);
  } ## end else [ if ($alert_filename)
}    #end while

###################
sub setup_logging {
###################
  # Set up logging. The log_config property should be a string
  # representing a Perl hash, e.g.
  # 'notifserver' => 2, 'NOCpulse::Notif::Escalator' => 4

  my $Log = NOCpulse::Log::Logger->new($program);

  # Don't prefix messages with the caller because this is the top call level.
  $Log->show_method(0);

  my $stream =
    NOCpulse::Log::LogManager->instance->stream(
                                                FILE       => $verboselog,
                                                APPEND     => 1,
                                                TIMESTAMPS => 1
                                               );
  $stream->autoflush(1);

  NOCpulse::Log::LogManager->instance->configure(eval($LOG_CONFIG))
    if $LOG_CONFIG;

  # The --log argument from the command line overrides the NOCpulse.ini
  # setting.
  NOCpulse::Log::LogManager->instance->configure(%{$logopt})
    if scalar(keys(%{$logopt}));

  return ($Log, $stream);
} ## end sub setup_logging

#####################
sub lock_alert_file {
#####################
  my $filename = shift;
  my $alert_file;
  my $tries = 0;

  while (!$alert_file && $tries <= 5) {
    $tries++;
    $alert_file = NOCpulse::Notif::AlertFile->open_file($filename);
    sleep(1) if ($tries > 1) && ($tries < 5);
  }

  return $alert_file;
} ## end sub lock_alert_file

#####################
sub process_backlog {
#####################
  my @list = shift;
  my @new_list;

  my $ref;
  while ($ref = shift(@list)) {
    my ($count, $method, @args) = @$ref;
    $Log->log(1, "processing backlog: ($count) $method(@args): ");
    eval {
      my $escalator_if =
        NOCpulse::Notif::EscalatorInterface->new(
                                           socket_filename => $SOCKET_FILENAME);
      $escalator_if->$method(@args);
    };
    if ($@ && $count < 5) {
      $Log->log(1, "failed\n");
      $count++;
      push(@new_list, [ $count, $method, @args ]);
    } else {
      $Log->log(1, "success\n");
    }
  } ## end while ($ref = shift(@list...
  return @new_list;
} ## end sub process_backlog

###########################
sub check_smtp_connection {
###########################
  my $smtp = shift;

  while (1) {
    unless ($smtp) {
      $Log->log(1, "Connecting to $MX\n");
      $smtp =
        Net::SMTP->new(
                       $MX,
                       Timeout => $TIMEOUT,
                       Debug   => 1
                      );
      unless ($smtp) {
        $Log->log(1, "ERROR: Cannot connect to $MX: $@... waiting\n");
        sleep(60);
        next;
      }
    } ## end unless ($smtp)
    if ($smtp->ok()) {
      last;
    } else {

      # Something is wrong, reconnect
      if ($smtp->hello($MX)) {

        # Got a good connection
        $Log->log(1, "WARNING: Reconnected to $MX\n");
        last;
      } else {
        $Log->log(1, "ERROR: Unable to reconnect to $MX, ",
                  $smtp->code, ": ", $smtp->message, "\n");
        $smtp = undef;
        sleep(60);
      }
    } ## end else [ if ($smtp->ok())
  } ## end while (1)
  return $smtp;
} ## end sub check_smtp_connection

__END__

=head1 NAME

notifier - Program that launches notification sends via SMTP and SNMP.

=head1 SYNOPSIS

/usr/bin/notifier

=head1 DESCRIPTION

notifier is invoked as a daemon.  It queries the notification escalator for new sends to launch and launches them as appropriate, registering the results of the sends back with the escalator for further action.

=head1 BUGS

No known bugs.

=head1 AUTHOR

Karen Jacqmin-Adams <kja@redhat.com>

Last update: $Date: 2005-02-17 20:28:49 $

=head1 SEE ALSO

B<NOCpulse::Notif::AlertFile>
B<NOCpulse::Notif::EscalatorInterface>
B<NOCpulse::Notif::Send>
B<notif-escalator>

=cut
