package Statusmon::bs_evgen;

#=============================================================================
#
# Id	  : @(#) $Id: bs_evgen.pm,v 1.27 2003/10/26 16:59:35 aeby Exp $
# Package : bb
# File	  : bb_evgen
# Author  : Thomas Aeby
# Date	  : 22.10.97
#
#
# $Log: bs_evgen.pm,v $
# Revision 1.27  2003/10/26 16:59:35  aeby
# page(): short text uses vars{hour}, vars{min}
#
# Revision 1.26  2003/10/26 16:56:58  aeby
# page(): format time ("%02d:%02d") as proposed in Bug #818955
#
# Revision 1.25  2003/07/31 14:05:45  aeby
# event(): remove alarms related with a host/check we are getting a "remove" event for
#
# Revision 1.24  2003/07/27 12:37:51  aeby
# page(): set "toaddress" variable so that skins get a chance to explicitly refer
# to the target mail address
# skins: mailalarm* now sets "To:" header field
#
# Revision 1.23  2003/04/23 21:31:12  aeby
# went through pattern matches: do a case invariant match if searching for a-z,
# more flexible pager:address match (allow everything besides ":" in pager names/paths)
#
# Revision 1.22  2003/03/02 17:35:51  aeby
# made "common.pm" a true module with package BigSister::common, all global
# variables are now in package BigSister::common
#
# Revision 1.21  2003/02/07 16:07:13  aeby
# compile(): do not try to interprete patterns like "host.test" as "status
# for host.test" - just interprete it as a string (Bug #682328)
#
# Revision 1.20  2002/04/22 21:41:06  aeby
# raise(): support "up=never" => never clear such an alarm automatically
#
# Revision 1.19  2002/03/17 15:20:59  aeby
# added DNS option
#
# Revision 1.18  2002/01/26 18:43:12  aeby
# allow inclusion of full host name/title in alarm messages
#
# Revision 1.17  2002/01/06 17:06:04  aeby
# introduced PAGER rules replacing notify.cfg functionality
#
# Revision 1.16  2002/01/04 22:17:37  aeby
# - @tmps handling: remember created temp files and remove them sometime later
# - default pager is 'smtpmail' if mailhost is set
# - when using the "background_process" method for invoking the pager pass the
#   input file via the INPUTFILE environment variable
#
# Revision 1.15  2002/01/04 17:30:20  aeby
# replaced page() method => use skins for generating alarm messages
#
# Revision 1.14  2001/12/10 15:29:17  aeby
# introduced new "virtual file system" within Big Sister code (%BigSister::common::fs)
# in order to allow implementing various path concepts (including FHS)
#
# Revision 1.13  2001/08/26 12:36:56  aeby
# added "upmail" variable in bs_evgen.pm
#
# Revision 1.12  2001/03/03 17:02:17  aeby
# altered support for host names containing "." characters: by default Big
# Sister does no name mangling any more (it's of no use anyway). There is
# an exception: when talking with Big Brother Big Sister replaces "."
# characters by ","
#
# Revision 1.11  2001/02/18 18:47:05  aeby
# introduced new function time_in_range(), use it in compile() for compiling
# "daytime xx:yy-pp:qq" pseudos. time_in_range() is based on time_at().
# (Bug #129080)
#
# Revision 1.10  2001/02/18 18:01:24  aeby
# added time_at() function, use time_at() in raise() for processing postpone_to
# config options
#
# Revision 1.9  2001/01/14 15:46:04  aeby
# added "maintenance mode":
#   - skins: added "white" lamps (aka. "maintenance")
#            added maintenance mode on alarm page
#   - common.pm: added "white" status
#   - bbd.pl/bsmon.pl: accept "*" wildcarded check in acknowledge messages
#   	   bsmon.pl: ignore status messages as long as a host/check is in
# 	   maintenance mode
#   - bswebalarm.pl: support for maintenance mode via alarm acknowledging and
#     via "Ignored Checks" form
#   - bs_evgen: accept maintenance acknowledges and provide "acknowledge"
#     feature provider API to other modules, set status of hosts/checks in
#     maintenance mode to white, status of hosts/checks leaving maintenance
#     mode to clear
#
# Revision 1.8  2000/11/12 17:24:28  aeby
# introduced Platform::system() call (fix for strange Win32 system())
#
# Revision 1.7  2000/11/12 16:47:25  aeby
# introduced Platform::replace function replacing rename().
#
# Revision 1.6  2000/10/12 09:38:53  aeby
# alarm queueing and removal: more verbose logging.
# raise(): stop removing alarms when (delay < now). This caused alarms to
# be accidentally dropped.
#
# Revision 1.5  2000/07/25 15:36:06  aeby
# platforms/*.pm: introduced $Platform::has_fork flag. Statusmon:
# introduced background() and abort() method. Rsync: Use background()
# method instead of calling fork(), work also if backgrounding is
# impossible (Windows NT). bs_evgen: do not flush states() if module
# was abort()-ed (e.g. when running in a child process).
#
# Revision 1.4  2000/07/25 15:08:37  aeby
# use the cfgfile module instead of the builtin config file reader
#
# Revision 1.3  2000/06/04 18:18:23  aeby
# use "grouping" feature provider instead of grouping file
#
# Revision 1.2  2000/04/21 17:03:01  aeby
# treat multiline text messages correctly
#
# Revision 1.1  2000/02/05 22:26:27  aeby
# initial revision of bsmonitor
#
# Revision 1.12  2000/01/02 15:27:47  aeby
# limit comment in SNMP traps to 150 chars
#
# Revision 1.11  1999/12/05 15:11:44  aeby
# added SNMP trap generation to bb_event_generator
#
# Revision 1.10  1999/10/24 12:38:36  aeby
# place alarms for checks listed in the ignore list in the 'new_alarms' list
# and keep them there until either the alarm condition or the ignore mode
# has gone. bswebalarm: show them as acknowledged
#
# Revision 1.9  1999/06/17 20:15:41  aeby
# on win32 systems use Platform::background_process for running the notifier
#
# Revision 1.8  1999/06/12 15:16:38  aeby
# tested on WinNT and fixed flaky implementations
#
# Revision 1.7  1999/06/11 17:58:34  aeby
# page(), raise(), ack(): include time in alarms sent
#
# Revision 1.6  1999/06/09 18:34:24  aeby
# do not use Socket module
#
# Revision 1.5  1999/06/07 18:47:03  aeby
# use log() to log event_generator events.
# assume checks are up when undefined (e.g. when used in "check=...")
#
# Revision 1.4  1999/06/07 18:14:19  aeby
# *** empty log message ***
#
# Revision 1.3  1999/05/09 17:14:43  aeby
# send "ack" alarm when receiving an acknowledgement
#
# Revision 1.2  1999/05/09 15:12:35  aeby
# completed support for alarm acknowledges
#
#
#=============================================================================

# check: condition met -> do not clear delayed alarm
# delay: delay in minutes
# repeat: warn every x minutes until alarm is cleared ("reminder")
# norepeat: do not raise another alarm for the same host/check within x minutes
# keep: keep alarm for at least x minutes even if it is cleared
# prio: alarm priority
# repeatprio: reminder priority
# mail: comma separated list of mail recipients

use strict;
use FileHandle;
use snmp;
use Statusmon::Statusmon;
use cfgfile;

my $tmpseq = 0;
my @tmps = ();

@Statusmon::bs_evgen::ISA = qw(Statusmon::Statusmon);


sub new
{

    my ($this) = @_;
    my  $class = ref($this) || $this;
    my  $self = {};

    bless($self, $class);

    $self->register( "statuschange" );
    $self->register( "alarm_acking" );
    $self->sched_frequency(1);
    $self->add_feature_provider( "acknowledging" );

    return($self);
}


sub start {
    my( $self ) = @_;
    $self->init();
    $self->load_rules();
#    $self->read_state();
    $self->load_state();
    $self->set_maint_mode();
    $self->{"active"} = $$;
}


sub run {
    my( $self ) = @_;

    $self->check_rulefile();
    # $self->check_logs();
    $self->check_alarms();
    # $self->check_state;
    $self->flush_state;
    for( my $i=$#tmps; $i>=0; $i-- ) {
	my $tmp = $tmps[$i];
	if( ($tmp->{"time"} < time)
	    && unlink( $tmp->{"file"} ) ) {
	    splice( @tmps, $i, 1 );
	}
    }
}


sub DESTROY {
    my( $self ) = @_;

    return if( $self->{"active"} != $$ );
    print STDERR "shutting down bs_evgen ...\n" if( $BigSister::common::dl>1 );
    $self->flush_state();
    foreach my $tmp (@tmps) {
	unlink( $tmp->{"file"} );
    }
}

    
sub init {
    my( $self ) = @_;

    @{$self->{"state_dirs"}} = ();
    @{$self->{"logs"}} = ();
    $self->{"pager"} = "";
    $self->{"pager_user"} = "";
    $self->{"rule_file"} = "";
    $self->{"rule_cfgfile"} = undef;
    $self->{"rule_file_stat"} = "";
    $self->{"group_file"} = "";
    $self->{"group_file_stat"} = "";
    $self->{"state_file"} = "";

    %{$self->{"numstates"}} = ( "red", 0, "yellow", 1, "purple", 2, "green", 3, "never", -1 );
    %{$self->{"textstates"}} = ( 0, "red", 1, "yellow", 2, "purple", 3, "green", -1, "never" );

    %{$self->{"previous_state"}} = ();
    @{$self->{"new_alarms"}} = ();
    @{$self->{"old_alarms"}} = ();
    @{$self->{"alarms"}} = ();
    %{$self->{"ign_acks"}} = ();
    %{$self->{"up"}} = ();
    $self->{"maint_mode"} = {};

    $self->{"minute"} = 60;
    $self->{"dirty"} = 1;

    my @state_dirs = ("$BigSister::common::fs{'web'}/logs");
    my $rule_file = "bb_event_generator.cfg";
    $self->{"pager"} = "/usr/lib/sendmail";
    $self->{"page_user"} = "alarm";

    if( $self->getarg("config") ) { $rule_file = $self->getarg("config"); }

    $self->{"rule_file"} = $rule_file;
    $self->{"rule_cfgfile"} = new cfgfile( $rule_file );
    $self->{"state_file"} = "$BigSister::common::fs{'var'}/bb_event_generator.state";
    $self->{"state_dirs"} = \@state_dirs;
    $self->{"override_dir"} = "$BigSister::common::fs{'web'}/logs/override";
}



sub check_rulefile {
    my( $self ) = @_;
    my $file;

    print "check_rulefile()\n" if( $BigSister::common::dl );
    if( (! $self->{"rule_cfgfile"}) || ($file = $self->{"rule_cfgfile"}->has_changed()) ) {
	BigSister::common::log( "notice", "config file $file has changed - reloading" ) if( $file );
	$self->load_rules();
    }
}


sub check_state {
    my( $self ) = @_;
    print "check_state()\n" if( $BigSister::common::dl );
    $self->read_state();
}


sub common_check {
    my( $self, $type, $i, $queue, $alarm ) = @_;

    if( (defined $alarm->{"ack_until"}) && ($alarm->{"ack_until"} < time) && ($alarm->{"ack_until"} > 0) ) {
	delete $alarm->{"ack_until"};
	delete $alarm->{"ack_user"};
	delete $alarm->{"ack_mode"};
	delete $alarm->{"ack_text"};
	$self->{"dirty"} = 1;
    }
    if( (defined $alarm->{"ack_mode"}) && ($alarm->{"ack_mode"} eq "del") ) {
	splice( @$queue, $i, 1 );
	$self->{"dirty"} = 1;
	&BigSister::common::log( "notice", "removing alarm for ".($alarm->{"host"}).".".($alarm->{"item"})." on user request" );
    }
}



sub check_alarms {
    my( $self ) = @_;
    my( $i, $var, $now );
    print "check_alarms()\n" if( $BigSister::common::dl );
    $now = time;
    for( $i=$#{$self->{"new_alarms"}}; $i>=0; $i-- ) {
	$var = $self->{"new_alarms"}->[$i];
	$self->common_check( "new", $i, $self->{"new_alarms"}, $var );
	if( (defined $$var{"check"}) && ! eval $$var{"check"} ) {
	    splice( @{$self->{"new_alarms"}}, $i, 1 );
	    print STDERR "dropping alarm since dependencies tell us to do so\n" if( $BigSister::common::dl );
	    &BigSister::common::log( "info", "dropping alarm for ".($var->{"host"}).".".$var->{"item"}." since dependencies tell us to do so" );
	    $self->{"dirty"} = 1;
	    next;
	}
	if( ($$var{"delay"}<$now) && ($$var{"postpone"}<$now) &&
	    (! defined $var->{"ack_mode"}) &&
	    (! defined $self->{"ign_acks"}->{($var->{"host"}).".".($var->{"item"})}) &&
	    (! defined $self->{"ign_acks"}->{($var->{"host"}).".*"}) ) {
	    splice( @{$self->{"new_alarms"}}, $i, 1 );
	    $var->{"_ignored"} = 0;
	    $self->alarm( $var );
	}
    }
    for( $i=$#{$self->{"alarms"}}; $i>=0; $i-- ) {
	$var = $self->{"alarms"}->[$i];
	$self->common_check( "active", $i, $self->{"alarms"}, $var );
	if( $$var{"_next_repetition"} && ($$var{"_next_repetition"} < $now) ) {
	    $self->alarm( $var, 1 );
	}
	if( $$var{"_dequeue"} && ($$var{"_dequeue"}<$now) 
	    && ($self->{"up"}->{$$var{"host"}.".".$$var{"item"}}) ) {
	    $self->clear_alarm( $i );
	}
    }
    for( $i=$#{$self->{"old_alarms"}}; $i>=0; $i-- ) {
	$var = $self->{"old_alarms"}->[$i];
	$self->common_check( "old", $i, $self->{"old_alarms"}, $var );
	if( $$var{"norepeat"} <= $now ) {
	    &BigSister::common::log( "notice", "forgetting old alarm: ".$$var{"host"}.".".$$var{"item"}." ".$$var{"text"} );
	    print "\x07forgetting old alarm: ".$$var{"host"}.".".$$var{"item"}." ".$$var{"text"}."\n" if( $BigSister::common::dl );
	    splice( @{$self->{"old_alarms"}}, $i, 1 );
	    &BigSister::common::log( "info", "forgetting old alarm for ".($var->{"host"}).".".$var->{"item"} );
	    $self->{"dirty"} = 1;
	}
    }
    while( ($i,$var) = each %{$self->{"ign_acks"}} ) {
	if( ($var->{"eol"}>0) && ($var->{"eol"}<time) ) {
	    delete $self->{"ign_acks"}->{$i};
	    $self->{"dirty"} = 1;
	    delete $self->{"maint_mode"}->{($var->{"host"}).".".($var->{"item"})};
	    $self->set_to( $var->{"host"}, $var->{"item"}, "clear", "maintenance is over" );
	}
    }
}



sub event {
    my( $self, $event ) = @_;

    if( $event->{"cmd"} eq "statuschange" ) {
	my $host = $event->{"host"};
	my $check = $event->{"check"};
	my $old = $event->{"oldstatuscode"};
	my $text = $event->{"text"};
	$text =~ s/\n/\|\>/g;
	unless( defined $old ) {
	    $old = $event->{"previous_state"}->{$host.".".$check};
	    ($old = $event->{"statuscode"}) unless( defined $old );
	}
	$self->raise( $host, $check, $old,
		      $event->{"statuscode"}, $text, $event->{"time"} );
	$self->{"dirty"} = 1;
	$self->{"previous_state"}->{$host.".".$check} 
	  = $event->{"statuscode"};
    }
    elsif( $event->{"cmd"} eq "remove" ) {
	my $host = $event->{"host"};
	my $check = $event->{"check"};
	foreach my $queuename qw( alarms new_alarms old_alarms ) {
	    my $queue = $self->{$queuename};
	    for( my $i=$#$queue; $i>=0; $i-- ) {
	        my $alarm = $queue->[$i];
	        if( ($alarm->{"host"} eq $host) && ($alarm->{"item"} eq $check)) {
		    splice( @$queue, $i, 1 );
		}
	    }
	}
	delete( $self->{"ign_acks"}->{"$host.$check"} );
	delete( $self->{"up"}->{"$host.$check"} );
	delete( $self->{"previous_state"}->{"$host.$check"} );
	$self->{"dirty"} = 1;
    }
    elsif( $event->{"cmd"} eq "ack" ) {
	my( $host, $item, $duration, $user, $what, $text ) = (
	    $event->{"host"},
	    $event->{"check"},
	    $event->{"duration"},
	    $event->{"user"},
	    $event->{"mode"},
	    $event->{"comment"} );
	my( $i, $ntime );
	$text =~ s/\n/\|\>/g;

	print "got ack for $host.$item from $user for $duration: $what: $text\n" if( $BigSister::common::dl>2 );
	&BigSister::common::log( "notice", "got ack for $host.$item from $user for $duration: $what: $text" );
	my $time;
	if( $duration eq "forever" ) {
	    $time = -1;
	    $ntime = time+365*24*3600;
	}
	elsif( $duration =~ /^(\d+):(\d+)$/ ) {
	$ntime = $time = time+(($1*60)+$2)*60;
	}
	else {
	    $ntime = $time = time+3600;
	    $duration = "unspecified";
	}
	foreach $i (@{$self->{"alarms"}}, @{$self->{"new_alarms"}}, @{$self->{"old_alarms"}}) {
	    next unless( ($i->{"host"} eq $host) && ($i->{"item"} eq $item) );
	    $i->{"ack_user"} = $user;
	    $i->{"ack_until"} = $time;
	    $i->{"ack_mode"} = $what;
	    $i->{"ack_text"} = $text;
	    $self->ack( $i );
	}
	if( ($what eq "ign") || ($what eq "maint") ) {
	    $self->{"ign_acks"}->{$host.".".$item} = {
		"eol" => $time,
		"host" => $host,
		"item" => $item,
		"user" => $user,
		"text" => $text,
		"until" => $time,
		"ack_user" => $user,
		"ack_mode" => $what,
		"ack_text" => $text,
		"ack_until" => $time
		};
	    $self->ack( $self->{"ign_acks"}->{$host.".".$item} );
	}
	if( $what eq "maint" ) {
	    if( ! $text ) {
		$text = "in maintenance".( ($time>0) ? (" until ".(scalar localtime($time))):"");
	    }
	    $self->set_to( $host, $item, "white", $text );
	    $self->{"maint_mode"}->{$host.".".$item} = 1;
	}
	$self->{"dirty"} = 1;
    }
    $self->make_dirty() if( $self->{"dirty"} );
}



sub set_to {
    my( $self, $host, $item, $color, $text ) = @_;

    if( $item eq "*" ) {
	foreach $item ($self->get_checks( $host )) {
	    $self->loopback( (time)." 127.0.0.1: status $host.$item $color (".(
	       time).") ".( scalar localtime )." $text" );
	}
     }
     else {
	$self->loopback( (time)." 127.0.0.1: status $host.$item $color (".(
	   time).") ".( scalar localtime )." $text" );
     }
}



sub set_maint_mode {
    my( $self ) = @_;
    my( $i, $vars );

    $self->{"maint_mode"} = {};
    while( ($i,$vars) = each %{$self->{"ign_acks"}} ) {
	if( $vars->{"ack_mode"} eq "maint" ) {
	    $self->{"maint_mode"}->{($vars->{"host"}).".".($vars->{"item"})} = 1;
	}
    }
}

sub load_state {
    my( $self ) = @_;
    my( $alrmvar, $txt );
    %{$self->{"previous_state"}} = ();
    %{$self->{"up"}} = ();
    open( F, "<".$self->{"state_file"} ) || return;
    while( <F> ) {
	chomp;
	if( /^status ([^\s]+) (.*)$/ ) {
	    $self->{"previous_state"}->{$1} = $2;
	}
	elsif( /^up ([^\s]+) (.*)$/ ) {
	    $self->{"up"}->{$1} = $2;
	}
	elsif( /^(.*alarms|.*acks) (.*)/ ) {
	    $alrmvar = $1;
	    $txt = $2;
	    my %alarm = ();
	    foreach (split("\x00",$txt)) {
		if( /^([^=]+)=(.*)$/ ) {
		    $alarm{$1} = $2;
		}
	    }
	    unless( defined $alarm{"id"} ) {
		$alarm{"id"} = $alarm{"host"}.".".$alarm{"item"};
	    }
	    $alarm{"queue"} = $alrmvar;
	    if( $alrmvar eq "alarms" ) {
		push( @{$self->{"alarms"}}, \%alarm );
	    }
	    elsif( $alrmvar eq "new_alarms" ) {
		push( @{$self->{"new_alarms"}}, \%alarm );
	    }
	    elsif( $alrmvar eq "old_alarms" ) {
		push( @{$self->{"old_alarms"}}, \%alarm );
	    }
	    elsif( $alrmvar eq "ign_acks" ) {
		$self->{"ign_acks"}->{($alarm{"host"}).".".($alarm{"item"})} = \%alarm;
	    }
	}
    }
    close F;
}



sub flush_state {
    my( $self ) = @_;
    my( $what, $status, $fh );

    return unless( $self->{"dirty"} );
    $fh = FileHandle->new();
    open( $fh, ">".$self->{"state_file"}.".tmp" );
    while( ($what,$status) = each %{$self->{"previous_state"}} ) {
	print $fh "status $what $status\n";
    }
    while( ($what,$status) = each %{$self->{"up"}} ) {
	print $fh "up $what ".($status?1:0)."\n";
    }
    $self->print_alarms( $fh, "alarms", @{$self->{"alarms"}} );
    $self->print_alarms( $fh, "new_alarms", @{$self->{"new_alarms"}} );
    $self->print_alarms( $fh, "old_alarms", @{$self->{"old_alarms"}} );
    $self->print_alarms( $fh, "ign_acks", values %{$self->{"ign_acks"}} );
    close $fh;
    Platform::replace( $self->{"state_file"}.".tmp", $self->{"state_file"} ) && ( $self->{"dirty"} = 0 );
}



sub make_dirty {
    my( $self ) = @_;

    $self->{"dirty"} = 1;
    if( time - $self->{"lastflush"} > 5 ) {
	$self->flush_state();
	$self->{"lastflush"} = time;
    }
}



sub print_alarms {
    my( $self, $fh, $what, @alarms ) = @_;
    my( $alarm );
    my( $var, $val );

    foreach $alarm (@alarms) {
	print $fh $what." ";
	while( ($var, $val) = each %$alarm ) {
	    print $fh "$var=$val\x00";
	}
	print $fh "\n";
    }
}



sub read_state {
    my( $self ) = @_;
    my( $dir, $file, $state, $prev, $color, $fulltext );

    print "read_state()\n" if( $BigSister::common::dl>1 );
    foreach $dir( @{$self->{"state_dirs"}} ) {
	print "reading state dir $dir\n" if( $BigSister::common::dl>2 );
	opendir( DIR, $dir );
	foreach $file (readdir( DIR )) {
	    next unless( (-f $dir."/".$file) && ($file =~ /\./) );
	    print "reading state file $file\n" if( $BigSister::common::dl>8 );
	    open( F, "<$dir/$file" );
	    $fulltext = "";
	    while( <F> ) {
		$fulltext .= $_;
	    }
	    close F;
	    chomp $fulltext;
	    $fulltext =~ s/\n/\|\>/g;
	    $state = $fulltext;
	    $state =~ /^([a-z]*)/i;
	    $state = $self->{"numstates"}->{$1};
	    print "state: $state\n" if( $BigSister::common::dl>15 );
	    $prev = $self->{"previous_state"}->{$file};
	    next unless( defined $state );
	    if( $prev != $state ) {
		my( $host, $item ) = BigSister::common::separate_service( $file );
		($prev = $state) unless( defined $prev );
		$self->raise( $host, $item, $prev, $state, $fulltext, time );
	    }
	    $self->{"previous_state"}->{$file} = $state;
	}
	close DIR;
    }
}
    

sub page {
    my( $self, $vars, $what ) = @_;
    my( $short, $time );

    my %myvars = %$vars;
    print "page()\n" if( $BigSister::common::dl>3 );

    my $prefix = "$what.";
    ($what eq "up") && ($prefix = "up");
    ($what eq "ack") && ($prefix = "ack_");
    foreach my $var ("text", "time", "mail", "status") {
	$myvars{"$var"} = $myvars{"${prefix}.$var"} if( defined $myvars{"${prefix}.$var"} );
	$myvars{"$var"} = $myvars{"${prefix}$var"} if( defined $myvars{"${prefix}$var"} );
    }

    $myvars{"textstatus"} = $BigSister::common::status_texts{$myvars{"status"}};
    $myvars{"text"} =~ s/^.*?\d\d:\d\d [0-9]{4}\s//;
    $myvars{"time"} = time unless( $myvars{"time"} );
    my( $sec, $min, $hour ) = localtime($myvars{'time'});
    $myvars{"localtime"} = scalar localtime( $myvars{'time'} );
    if( defined $myvars{"ack_until"} ) {
	if( $myvars{"ack_until"} < 0 ) {
	    $myvars{"ack_until_localtime"} = "forever";
	}
	else {
	    $myvars{"ack_until_localtime"} = scalar localtime( $myvars{"ack_until"} );
	}
    }
    $myvars{"hour"} = sprintf( "%02d", $hour );
    $myvars{"minute"} = sprintf( "%02d", $min );
    my $text = $myvars{"text"};
    if( $text =~ /\&html/ms ) {
	$myvars{"text"} = $`;
    }
    $myvars{"htmltext"} = $text = $Statusmon::display->fixHTMLinText( $text, 1 );
    $myvars{"short"} = $myvars{"text"};
    $myvars{"short"} = substr( $myvars{"short"}, 0, $myvars{"msgmax"} ) if( $myvars{"msgmax"} );
    $myvars{"text"} =~ s/\|\>/\n/g;
    ($myvars{"severity"} = 50) unless( $myvars{"severity"} ); 

    my $grouping = $self->get_feature_provider( "grouping" );
    if( $grouping ) {
	$myvars{"host_description"} = $grouping->get_title( $myvars{"host"} );
    }

    my @users = split( ",", $myvars{"mail"} );
    my $pager = $myvars{"pager"};
    (@users = ($self->{"page_user"})) unless( @users );
    unless( $pager ) { 
	$pager = $myvars{"mailhost"}?"smtpmail":$self->{"pager"};
    };
    &BigSister::common::log( "notice", "sending page for $myvars{'host'}.$myvars{'item'} with severity $myvars{'severity'} via $pager to ".join(",",@users).": $myvars{'short'}" );

    #
    # loop through all the recipients
    foreach my $user (@users) {
	my %vars = %myvars;
	if( $user =~ /^([^:]+):(.*)$/i ) {
	    # if address is pager:address ...
	    $vars{"mail"} = $2;
	    $vars{"pager"} = $1;
	}
	else {
	    $vars{"mail"} = $user;
	    $vars{"pager"} = $pager;
	}
	foreach my $match (@{$self->{"rules"}}) {
	     next unless( $match->[0] eq "PAGER" && ! $match->[1] );
	     next unless( eval($match->[3]) );
	     my $ref = $match->[2];
	     while( my($var,$cont) = each %$ref ) {
		 $vars{$var} = $cont;
	     }
	}
	my $pager = $vars{"pager"};
	my @users = split( ",", $vars{"mail"} );
	my @cmd = ($pager, @users);
	$vars{"toaddress"} = "";
	foreach my $user (@users) {
	    $vars{"toaddress"} .= ", " if( $vars{"toaddress"} );
	    $vars{"toaddress"} .= "<$user>";
	} 
	if( $pager =~ /^(|.*\/\\)(notify|log_mail)$/ ) {
	    my $short = $vars{"host"}.".".$vars{"item"}." $what $vars{hour}.$vars{min}: ".$vars{"short"};
	    if( $what eq "ack" ) {
		$short = $vars{"ack_mode"}." by ".$vars{"ack_user"}." ".$short;
	    }
	    $short = substr( $short, 0, $vars{"msgmax"} ) if( $vars{"msgmax"} );
	    @cmd = ( $pager, "-S", $vars{"severity"}, "-s", $short, 
		    "-h", $vars{"host"}, "-m", $vars{"text"},
		    @users );
	    if( $vars{"mailhost"} ) {
		push( @cmd, "-M", $vars{"mailhost"} );
	    }
	}
	my $skin = {};
	if( $vars{"skin"} ) {
	    $skin = $Statusmon::display->read_skin( $vars{"skin"} );
	}
	else {
	    my $config = $self->get_feature_provider( "displayconfig" );
	    if( $config ) {
		$skin = $self->{"config"}->{"logskin"};
	    }
	}
	$skin = $Statusmon::display->read_skin( "default" ) unless( defined $skin->{"mailalarm_$what.proto"} );
	my $mailtext = $Statusmon::display->replace_vars( $skin, \%vars, $skin->{"mailalarm_$what.proto"} );
	while( my($var,$text) = each %vars ) {
	     $ENV{"BS_EV_$var"} = $text;
	}
	if( $Platform::has_fork ) {
	    my $handler = fork();
	    if( ! defined $handler ) {
		BigSister::common::log( "err", "cannot send alarm ... cannot fork:$!" );
	    }
	    # parent does just continue, child handles sending alarm
	    elsif( ! $handler ) {
		my $fh = new FileHandle();
		my $pid = open( $fh, "|-" );
		unless( defined $pid ) {
		    BigSister::common::log( "err", "cannot send alarm ... cannot fork: $!" );
		}
		elsif( $pid ) {
		    print $fh $mailtext;
		    close $fh;
		    if( $? ) {
			BigSister::common::log( "err", "failed to send alarm: command $cmd[0] terminated with return code ", $?>>8 );
		    }
		}
		else {
		    exec( @cmd );
		    exit(1);
		}
		exit(0);
	    }
	}
	else {
	    my $tmp = $BigSister::common::fs{"tmp"}."/bsev.$$.".($tmpseq++);
	    push( @tmps, {
		"file" => $tmp,
		"time" => time+90
	    } );
	    unless( 
		open( TMP, ">$tmp" )
		&& (print TMP $mailtext)
		&& close( TMP ) ) {
		BigSister::common::log( "err", "cannot send alarm - cannot create temp file $tmp: $!" );
	    }
	    my $cmd = shift @cmd;
	    my $args = $cmd;
	    while( @cmd ) {
		my $arg = shift @cmd;
		$arg =~ s/"/'/g;
		$args .= " \"$arg\"";
	    }
	    $ENV{"INPUTFILE"} = Platform::filepath( $tmp );
	    Platform::background_process( $cmd, $args );
	    delete $ENV{"INPUTFILE"};
	}
	while( my($var,$text) = each %vars ) {
	     delete $ENV{"BS_EV_$var"};
	}
    }
}



sub raise {
    my( $self, $host, $item, $old_state, $new_state, $text, $time ) = @_;
    my( $addr, @matches, %vars, $match, $ref, $var, $cont, $up );
    my( $i );
    my $now = time;

    print "raise( $host, $item, $old_state, $new_state, $text )\n" if( $BigSister::common::dl>2 );
    &BigSister::common::log( "info", "status for $host.$item changed from $old_state to $new_state" );
    if( $old_state<$new_state ) {

	for( $i=$#{$self->{"new_alarms"}}; $i>=0; $i-- ) {
	    $var = $self->{"new_alarms"}->[$i];
	    if( (($$var{"host"} eq $host) && ($$var{"item"} eq $item)
		  && ($$var{"up"}<=$new_state))
		|| (! eval( $$var{"check"} )) ) {
		splice( @{$self->{"new_alarms"}}, $i, 1 );
		print STDERR "dropping obsolete alarm from delayed queue\n" if( $BigSister::common::dl );
		&BigSister::common::log( "info", "dropping alarm for ".($var->{"host"}).".".$var->{"item"}." from delayed queue" );
		$self->{"dirty"} = 1;
	    }
	}
	for( $i=$#{$self->{"alarms"}}; $i>=0; $i-- ) {
	    $var = $self->{"alarms"}->[$i];
	    if( (($$var{"host"} eq $host) && ($$var{"item"} eq $item)
		  && ($$var{"up"}<=$new_state)) ) {
		$var->{"uptext"} = $text;
		$var->{"upstatus"} = $new_state;
		$var->{"uptime"} = $time;
	        $self->clear_alarm( $i );
	    }
	}
	$self->make_dirty() if( $self->{"dirty"} );
    }
    my @addrs;
    my $config = $self->get_feature_provider( "displayconfig" );
    if( $config && $config->get_current_config()->{"options"}->{"DNS"} ) {
	my ($name,$aliases,$addrtype,$length,@addrs) = gethostbyname( $host );
	foreach $addr (@addrs) {
	    $addr = join( ".", unpack( "C*", $addr ) );
	}
    }
    %vars = ( "id", $host.time.$item, "host", $host, "item", $item, "text", $text, "status", $new_state, "time", $time );
    foreach $match (@{$self->{"rules"}}) {
	 next unless( $self->match_rule( $match, $host, $item, \@addrs ) );
	 next unless( eval($$match[3]) );
	 $ref = $$match[2];
	 while( ($var,$cont) = each %$ref ) {
	     $vars{$var} = $cont;
	 }
    }
    if( $BigSister::common::dl>3 ) {
	print "matched rule: ";
	while( ($var,$cont) = each %vars ) {
	    print "$var=$cont, ";
	}
	print "\n";
    }
    unless( defined $vars{"down"} ) {
	    $vars{"down"} = 1;
    }
    unless( defined $vars{"up"} ) {
	    $vars{"up"} = $vars{"down"}+1;
    }
    # if up=never set up to an incredibly high (never reached) value
    # up=never alarms will never be cleared automatically - only via ack'ing
    if( $vars{"up"} == -1 ) {
        $vars{"up"} = 1000;
    }
    if( $new_state>$old_state ) {
	$up = $new_state>=$vars{"up"};
    }
    else {
	$up = $new_state>$vars{"down"};
    }
    $self->{"up"}->{$host.".".$item} = $up;
    return if( ($old_state <= $new_state) || $up );
    if( $vars{"check"} ) {
	$vars{"check"} = $self->compile( $vars{"check"}, \%vars );
    }
    else {
	$vars{"check"} = 1;
    }
    return unless( (eval $vars{"check"}) );
    if( grep( ($host eq $$_{"host"}) && ($item eq $$_{"item"}), 
	@{$self->{"new_alarms"}}, @{$self->{"alarms"}}, @{$self->{"old_alarms"}} ) ) {
	print "no alarm since alarm pending/norepeated\n" if( $BigSister::common::dl>2 );
	return;
    }
    if( $vars{"keep"} ) {
	$vars{"keep"} = time + $self->{"minute"}*$vars{"keep"};
    }
    unless( $vars{"repeatprio"} ) {
	$vars{"repeatprio"} = $vars{"prio"};
    }
    if( $vars{"postpone_to"} ) {
	if( $vars{"postpone_to"} =~ /^(\d+):(\d+)$/ ) {
	    my $sec = time_at( $vars{"postpone_to"}, 1 ) - time;
	    $vars{"postpone"} = $sec/60;
	    print "time update: $sec\n";
	}
    }
    if( (defined $self->{"ign_acks"}->{$host.".".$item})
	|| defined $self->{"ign_acks"}->{$host.".*"}) {
	$vars{"_ignored"} = 1;
    }
    if( $vars{"_ignored"} || $vars{"delay"} || $vars{"postpone"} ) {
	$vars{"delay"} = time+$self->{"minute"}*$vars{"delay"} if( $vars{"delay"} );
	$vars{"postpone"} = time+$self->{"minute"}*$vars{"postpone"} if( $vars{"postpone"} );
	$vars{"delay"} = 0 unless( $vars{"delay"} );
	$vars{"postpone"} = 0 unless( $vars{"postpone"} );
	if( $vars{"norepeat"} ) {
	    $vars{"norepeat"} = time + $self->{"minute"}*$vars{"norepeat"};
	    $self->{"dirty"} = 1;
	}
	$vars{"queue"} = "new_alarms";
	push( @{$self->{"new_alarms"}}, \%vars );
	$self->{"dirty"} = 1;
	print "goes to delayed alarm queue\n" if( $BigSister::common::dl>2 );
    }
    else {
	$self->alarm( \%vars );
    }
    $self->make_dirty() if( $self->{"dirty"} );
}



sub alarm {
    my( $self, $vars, $repeat ) = @_;

    print "\x07raising alarm: ".$$vars{"host"}.".".$$vars{"item"}." ".$$vars{"text"}."\n" if( $BigSister::common::dl );
    my $short = substr( $$vars{"text"}, 0, 60 );
    &BigSister::common::log( "notice", "raising alarm: ".$$vars{"host"}.".".$$vars{"item"}." ".$short );
    unless( grep( $_ eq $vars, @{$self->{"alarms"}} ) ) {
	$vars->{"queue"} = "alarms";
	push( @{$self->{"alarms"}}, $vars );
	$self->{"dirty"} = 1;
    }
    if( $$vars{"repeat"} ) {
	($$vars{"_next_repetition"} = time) unless( $$vars{"_next_repetition"} );
	$$vars{"_next_repetition"} += $$vars{"repeat"}*$self->{"minute"};
	$self->{"dirty"} = 1;
    }
    return if( $vars->{"ack_mode"} && $repeat );
    $self->page( $vars, $repeat?"reminder":"down" );
#    $self->page( $$vars{"pager"}, $$vars{"host"}, $$vars{"item"}, $$vars{"host"}.":".$$vars{"item"}, $repeat?$$vars{"repeatprio"}:$$vars{"prio"}, $repeat?"reminder":"down", $$vars{"text"}, $$vars{"msgmax"}, $$vars{"mailhost"}, split(",",$$vars{"mail"}) );
    if( $vars->{"trap"} ) {
	snmp::trap( 
	    $vars->{"trap"},
	    undef,
	    $vars->{"host"},
	    $repeat?"bsremind":"bsservicedown",
	    {
		"bshost" => [ "string", $vars->{"host"}],
		"bsservice" => [ "string", $vars->{"item"}],
		"bsstatus" => [ "string", $self->{"textstates"}->{$vars->{"status"}}],
		"bscomment" => [ "string", substr($vars->{"text"},0,150)],
		"bsseverity" => [ "int", $repeat?$$vars{"repeatprio"}:$$vars{"prio"} ]
	    } );
    }
}



sub ack {
    my( $self, $vars ) = @_;

    my $text = $vars->{"ack_text"};
    my $ack = ($vars->{"ack_mode"})." by ".($vars->{"ack_user"}).": ";
    unless( $text =~ s/^(.*?[0-9]:[0-9]{2} [0-9]{4}\s)/$1 $ack/ ) { $text = $ack.$text; } 
    $self->page( $vars, "ack" );
#    $self->page( $$vars{"pager"}, $$vars{"host"}, $$vars{"item"}, $$vars{"host"}.":".$$vars{"item"}, $$vars{"prio"}, "ack", $text, $$vars{"msgmax"}, $$vars{"mailhost"}, split(",",$$vars{"mail"}) );
    if( $vars->{"trap"} ) {
	snmp::trap( 
	    $vars->{"trap"},
	    undef,
	    undef,
	    "bsalarmack",
	    {
		"bshost" => [ "string", $vars->{"host"}],
		"bsservice" => [ "string", $vars->{"item"}],
		"bscomment" => [ "string", $vars->{"ack_text"}],
		"bsackmode" => [ "string", $vars->{"ack_mode"}],
		"bsackuser" => [ "string", $vars->{"ack_user"}]
	    } );
    }
}



sub clear_alarm {
    my( $self, $i ) = @_;
    my( $vars );

    $vars = $self->{"alarms"}->[$i];
    print "\x07clearing alarm: ".$$vars{"host"}.".".$$vars{"item"}." ".$$vars{"text"}."\n" if( $BigSister::common::dl );
    my $short = substr( $$vars{"text"}, 0, 60 );
    &BigSister::common::log( "info", "clearing alarm: ".$$vars{"host"}.".".$$vars{"item"}." ".$short );
    if( $$vars{"keep"} && ($$vars{"keep"}>time) ) {
	$$vars{"_dequeue"} = $$vars{"keep"};
	print "keep alarm since 'keep' was defined\n" if( $BigSister::common::dl>1 );
	return;
    }
    splice( @{$self->{"alarms"}}, $i, 1 );
    $self->{"dirty"} = 1;
    if( $$vars{"norepeat"} && ($$vars{"norepeat"}>time) ) {
	$vars->{"queue"} = "old_alarms";
	push( @{$self->{"old_alarms"}}, $vars );
    }
    my $mail = $$vars{"upmail"};
    if( ! $mail ) {
	$mail = $$vars{"upmail"} = $$vars{"mail"};
    }
    $self->page( $vars, "up" );
    if( $vars->{"trap"} ) {
	snmp::trap( 
	    $vars->{"trap"},
	    undef,
	    $vars->{"host"},
	    "bsserviceup",
	    {
		"bshost" => [ "string", $vars->{"host"}],
		"bsservice" => [ "string", $vars->{"item"}],
		"bsstatus" => [ "string", $self->{"textstates"}->{$vars->{"upstatus"}}],
		"bscomment" => [ "string", substr($vars->{"uptext"},0,100)],
		"bsseverity" => [ "int", $$vars{"prio"} ]
	    } );
    }
    $self->make_dirty() if( $self->{"dirty"} );
}


sub compile {
    my( $self, $check, $vars ) = @_;
    my( $s );
    my %days = ( "Sun", 0, "Mon", 1,
		 "Tue", 2, "Wed", 3,
		 "Thu", 4, "Fri", 5,
		 "Sat", 6 );

    $s = "";
    print "compile( $check )\n" if( $BigSister::common::dl>3 );
    while( $check =~ /\$([a-z]+)/i ) {
	if( $vars ) {
	    $s .= $`.$$vars{$1};
	}
	else {
	    $s .= $`."\$vars{'$1'}";
	}
	$check = $';
    }
    $check = $s.$check;
    $s = "";
    while( $check =~ /(daytime|weekday)[\s\t]+([0-9:a-z\-,]+)/i ) {
	$s .= $`;
	$check = $';
	my ($cmd,$args) = ($1,$2);
	if( $cmd eq "daytime" ) {
	    unless( $args =~ /^(\d+):(\d+)-(\d+):(\d+)$/ ) {
		print STDERR "$BigSister::common::prog: wrong time format (should be hh:mm-hh:mm) in daytime in line $.\n";
		$args = 0;
	    }
	    else {
		my $start = "$1:$2";
		my $end = "$3:$4";
		$args = "(time_in_range( '$start', '$end' ))";
  	    }
	}
	elsif( $cmd eq "weekday" ) {
	    my @days = (0,0,0,0,0,0,0);
	    my $expr;
	    foreach $expr (split(",",$args)) {
		if( defined $days{$expr} ) {
		    $days[$days{$expr}] = 1;
		}
		elsif( ($expr =~ /(.*)-(.*)/) && defined $days{$1} && defined $days{$2} ) {
		    my ($start,$end) = ($days{$1},$days{$2});
		    $days[$start] = 1;
		    while( $start != $end ) {
			$start++;
			$start=0 if( $start>6 );
			$days[$start] = 1;
		    }
		}
		else {
		    print STDERR "$BigSister::common::prog: wrong day expression in weektime in line $.\n";
		}
	    }
	    $args = "((".join(",",@days).")[((localtime)[6])])";
	}
	$s .= $args;
    }
    $check = " ".$s.$check;
    $s = "";
    while( $check =~ /(.)([a-z:_0-9\-\.]*)\.([a-z:_0-9\-]+)/i ) {
        if( ($1 eq '"') || ($1 eq "'") ) {
	    $s = $`.$1.$2.".".$3;
	    $check = $';
        }
	else {
	    $s .= $`.$1."((! defined \$self->{\"up\"}->{\"$2.$3\"}) || \$self->{\"up\"}->{\"$2.$3\"})";
	    $check = $';
	}
    }
    $check = $s.$check;
    print "compiled to $check\n" if( $BigSister::common::dl>4 );
    return $check;
}



sub match_rule {
    my( $self, $rule, $host, $item, $addrs ) = @_;
    my( $rhost, $ritem, $vars ) = @$rule;
    my( $ref );

    return(0) if( ($ritem ne "*") && ($ritem ne $item) && ($item || $ritem) );
    if( substr( $rhost, 0, 1 ) eq "[" ) {
	$rhost = substr( $rhost, 1, length($rhost)-2 );
	return(0) unless( grep( substr( $_.".", 0, length($rhost)+1 ) eq $rhost.".", @$addrs ) );
    }
    elsif( substr( $rhost, 0, 1 ) eq "@" ) {
	$rhost = substr( $rhost, 1 );
	my $grouper = $self->get_feature_provider( "grouping" );
	return(0) unless( $grouper && $grouper->isingroup( $host, $rhost ) );
	print "$host is in group $rhost\n" if( $BigSister::common::dl>6 );
    }
    else {
	return(0) if( ($rhost ne "*") && ($rhost ne $host) );
    }
    return 1;
}





sub load_rules {
    my( $self ) = @_;
    my( $var, $match, $fh, $chk, $pre_condition );
    my( $host, $item );

    print "loading rule file $self->{rule_file}\n" if( $BigSister::common::dl>1 );
    $self->{"rules"} = [];
    $self->{"rule_cfgfile"}->rewind();
    while( $_ = $self->{"rule_cfgfile"}->nextline() ) {
	chomp;
	next if( /^$/ );
	if( /^([^\s\t]+)/ ) {
	    $match = $1;
	    $var = $';
	    if( ($match =~ /\{/) && ! ($match =~ /\}/) && ($var =~ /^(.*?\})/) ) {
		$match .= $1;
		$var = $';
	    }
	    if( $match =~ /\{(.*)\}/ ) {
		$match = $`.$';
		$pre_condition = $self->compile( $1 );
	    }
	    else {
		$pre_condition = 1;
	    }
	    my @tok = split( /\./, $match );
	    if( $#tok > 0 ) {
		my $check = pop @tok;
		$match = join( $BigSister::common::domain_encode, @tok ).".".$check;
	    }
	}
	else {
	    next;
	}
	print "adding rule match: $match, precondition: $pre_condition, vars: $var\n" if( $BigSister::common::dl>5 );
	my %vars = ();
	while( $var ) {
	    $var =~ s/^[\s\t]+//;
	    if( ($var =~ /^([a-z0-9_]+)[\s\t]*=[\s\t]*\"([^\"]*)\"/i)
	    	|| ($var =~ /^([a-z0-9_]+)[\s\t]*=[\s\t]*([^\s\t]+)/i) ) {
		$vars{$1} = $2;
		$var = $';
	    }
	    else {
		last;
	    }
	}
	if( $vars{"down"} ) {
	    $vars{"down"} = $self->{"numstates"}->{$vars{"down"}};
	}
	if( $vars{"up"} ) {
	    $vars{"up"} = $self->{"numstates"}->{$vars{"up"}};
	}
	foreach $chk (split( ";", $match )) {
	    ($host, $item) = BigSister::common::separate_service( $chk );
	    push( @{$self->{"rules"}}, [ $host, $item, \%vars, $pre_condition ] );
	}
    }
    return 1;
}



sub time_in_range {
    my( $start, $end ) = @_;

    $start = time_at( $start );
    $end = time_at( $end );
    if( $end < $start ) { $end += 24*3600; }
    if( time < $start ) {
	$start -= 24*3600;
	$end -= 24*3600;
    }
    return( (time>=$start) && (time<=$end) );
}



sub time_at {
    my( $daytime, $infuture, $now ) = @_;
    my( $newhour, $newmin );

    if( $daytime =~ /^(\d+):(\d+)$/ ) {
	$newhour = $1;
	$newmin = $2;
	while( $newhour >= 24 ) { $newhour -= 24; }
    }
    else {
	return undef;
    }
    ($now = time) unless( $now );
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($now); 

    my $diff = ($newhour*3600 + $newmin*60) - ($hour*3600 + $min*60 + $sec);
    if( ($now + $diff < time) && $infuture ) {
	$diff += 24*3600;
    }
    ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($now+$diff); 
    $diff += ($newhour*3600 + $newmin*60) - ($hour*3600 + $min*60 + $sec);

    return( $now + $diff );
}

#print ((scalar localtime( time_at( "24:00", 0, time+0*3600*24 ) ))."\n");

sub abort {
    my( $self ) = @_;

    $self->{"active"} = 0;
}



# ============== feature provider stuff =========================

sub is_maintenance {
    my( $self, $host, $check ) = @_;

    return( (defined $self->{"maint_mode"}->{$host.".*"}) ||
	    (defined $self->{"maint_mode"}->{$host.".".$check}) );
}


1;
