#!/usr/bin/perl
#    Big Sister network monitor
#    Copyright (C) 2000  Thomas Aeby
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#

#=============================================================================
#
my $Usage	  = "[-D level] [-f cfgfile]";
#
#=============================================================================
@BigSister::common::options = ( "f:s" );
use lib "."; use lib "uxmon"; use lib "platforms"; use lib "/usr/local/lib/bs/bin"; 		#inslib
use BigSister::common;
use Platforms;
use strict;
use FileHandle;
use Schedule;
use Statusmon::Statusmon;
use cfgfile;
use MicroTime;
proginit();
Statusmon::Statusmon::init();


%BigSister::common::cmd_groups = (
    "all" =>            0xFFFFFFFF,
    "authenticate" =>   0x00000001,
    "status" =>         0x00000002,
    "grouping" =>       0x00000004,
    "page" =>           0x00000008,
    "archiving" =>      0x00000010,
    "alarm_acking" =>   0x00000020,
    "perf" =>           0x00000040,
    "statuschange" =>   0x00000080,
    "textchange" =>	0x00000100,
    "metagrouping" =>	0x00000200,
    "config" =>		0x00000400
);

%BigSister::common::current_status = ();
%BigSister::common::current_status_c = ();
%BigSister::common::current_text = ();

my $dl = $BigSister::common::dl;
my $cfgfile = $BigSister::common::opt_f;
my @monitors = ();
my $eventid = 1;
my $acknowledge_provider = undef;

Platform::mergepath( $BigSister::common::fs{"bin"} );
$SIG{"ALRM"} = $SIG{"PIPE"} = "IGNORE";
$SIG{"TERM"} = $SIG{"INT"} = $SIG{"QUIT"} = \&sigdie;
$SIG{"INT"} = "IGNORE" unless( $dl );

$BigSister::common::on_unix = Platforms::isunix();

unless( $cfgfile ) {
    $cfgfile = "bsmon.cfg";
}

read_status();

my $statusfile = "$BigSister::common::fs{'var'}/status.log";
unless( read_config( $cfgfile ) ) {
    BigSister::common::log( "err", "no configuration file" );
    exit(1);
}
    

&background unless( $BigSister::common::dl );

my $fh = new FileHandle;
my $fopen = 0;
my $fwarned = 0;
my $pos = 0;
my $checkrotate = 10;
my $now_ino = 0;
my $now_size = 0;
my $sched_time = time;
my $max;
my $flush = 30;
my $firstpass = 1;
my $startpos = 0;
my $now;
my $timewarn = 0;
my $rotatewait = 0;

while(1) {
    while( $sched_time < time ) {
	my $oldprof = $Scheduler::profile;
	$Scheduler::profile = get_current_config()->{"options"}->{"Profile"};
	Schedule::schedule( \@monitors, $sched_time );
	$sched_time += 60;
	if( $flush-- < 0 ) {
	    flush_status();
	    $flush = 30;
	}
	$Scheduler::profile = $oldprof;
    }

    readlog() unless( $rotatewait );

    if( ($checkrotate-- <= 0) || $rotatewait ) {
	$checkrotate = 10;
	my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
	  $atime,$mtime,$ctime,$blksize,$blocks)
	    = stat($statusfile);
	if( Platforms::iswin32() ) {
	    $ino = $now_ino = 1;
	}
	# do not compare with $pos - since some OS have attribute
	# caches making stat() returning old information
	if( ($size < $now_size) || ($ino != $now_ino) ) {
	    BigSister::common::log( "info", "status.log rotated (size: $size, pos: $pos)" );
	    close( $fh );
	    undef $fopen;
	    $rotatewait = 0;
	}
	elsif( $rotatewait && ($size > $pos) ) {
	    # data source decided to fall back to the existing log file
	    BigSister::common::log( "info", "status.log re-attached (size: $size, pos: $pos)" );
	    $rotatewait = 0;
	    $startpos = $pos;
	}
	$now_size = $size;
    }
    waitchildren();
    sleep 5;
}


sub waitchildren {
    my( $max, $pid );
    while( (($pid=Platform::bswait_nohang()) != -1) && ($max++<10) ) {
	print "child $pid terminated\n" if( $dl>2 );
    }
}
    

sub readlog {
    unless( $fopen ) {
	if( open( $fh, "<$statusfile" ) ) {
	    seek( $fh, 0, $firstpass?2:0 );
	    $firstpass = 0;
	    $pos = tell( $fh );
	    if( $startpos ) {
		$pos = $startpos;
		$startpos = 0;
	    }
	    $fwarned = 0;
	    $now_ino = ((stat( $statusfile ))[1]);
	    $fopen = 1;
	}
	else {
	    BigSister::common::log( "err", "cannot open status file $statusfile" ) unless( $fwarned );
	    $fwarned = 1;
	    sleep 5;
	    return;
	}
    }
    seek( $fh, $pos, 0 ) || return;
    my $i = 0;
    while( $fopen && ($_ = <$fh>) ) {
	if( $i++ > 10 ) {
	    waitchildren();
	    $i = 0;
	}
	$now = time;
	last unless( /[\r\n]$/ );
	$pos = tell( $fh );
	chomp;
	my $time = decode( $_ );
	if( (defined $time) && ($now-$time > 300) ) {
	    my $lag = int(($now - $time)/60);
	    if( $lag>=7 ) {
		BigSister::common::log( "err", "ERROR: bsmon lags behind more than 7 minutes ($lag) - dropping messages" );
		while( <$fh> ) {
		    ;
		}
		$timewarn = 0;
		return;
	    }
	    BigSister::common::log( "warn", "WARNING: bsmon lags behind more than 5 minutes" ) unless( $timewarn );
	    $timewarn = 1;
	}
    }
}


sub decode {
    my( $command ) = @_;
    my %cmd;
    my $check;

    print STDERR "decoding ".$command."\n" if( $dl > 5 );
    return unless( $command =~ /^(\d+) (.*?): ([a-z]+)(|\+\d+) +(.*)/ );
    $cmd{"time"} = $1;
    $cmd{"agent"} = $2;
    $cmd{"cmd"} = $3;
    $cmd{"expires"} = $4;
    $cmd{"args"} = $5;
    $cmd{"args"} =~ s/\|\>/\n/g;
    $cmd{"id"} = $eventid++;

    print "got a ".$cmd{"cmd"}." log file entry\n" if( $dl>7 );

    if( $cmd{"cmd"} eq "status" ) {
	return unless( $cmd{"args"} =~ /^([^ ]+)\.([^ \.]+) ([^\s]+)\s*(.*)$/s );
	$cmd{"host"} = $1;
	$cmd{"check"} = $2;
	$cmd{"status"} = $3;
	$cmd{"text"} = $cmd{"rawtext"} = $4;
	if( $acknowledge_provider &&
	    $acknowledge_provider->is_maintenance( $cmd{"host"}, $cmd{"check"} ) ) {
	    return( $cmd{"time"} );
	}
	if( $cmd{"text"} =~ /^\((\d+)\) / ) {
	    $cmd{"text"} = $';
	    $cmd{"agenttime"} = $1;
	}
	if( $cmd{"text"} =~ /^(.*?)(\d\d \d\d\d\d) (.*)$/s ) {
	    $cmd{"text"} = $3;
	    $cmd{"date"} = $1.$2;
	}
	$cmd{"statuscode"} = $BigSister::common::status_codes{$cmd{"status"}};
	$cmd{"cmd_group"} = "status";
	my $check = $cmd{"host"}.".".$cmd{"check"};
	if( ! defined $BigSister::common::current_status_c{$cmd{"host"}} ) {
	    $BigSister::common::current_status_c{$cmd{"host"}} = {};
	}
	$BigSister::common::current_status_c{$cmd{"host"}}->{$cmd{"check"}} = $cmd{"statuscode"};
	if( defined $BigSister::common::current_status{$check} ) {
	    $cmd{"statussince"} = $BigSister::common::current_status{$check}->{"time"};
	}
	if( defined $BigSister::common::current_text{$check} ) {
	    $cmd{"textsince"} = $BigSister::common::current_text{$check}->{"time"};
	}
	if( (! defined $BigSister::common::current_status{$check}) || ($BigSister::common::current_status{$check}->{"status"} != $cmd{"statuscode"}) ) {
	    my %ccmd = %cmd;
	    $ccmd{"cmd"} = "statuschange";
	    $ccmd{"cmd_group"} = "statuschange";
	    if( defined $BigSister::common::current_status{$check} ) {
		$ccmd{"oldstatuscode"} = $BigSister::common::current_status{$check}->{"status"};
		$ccmd{"oldtime"} = $BigSister::common::current_status{$check}->{"time"};
		$ccmd{"oldstatus"} = $BigSister::common::status_texts{$ccmd{"oldstatuscode"}};
		$cmd{"statussince"} = $ccmd{"statussince"} = $cmd{"time"};
	    }
	    elsif( get_current_config()->{"options"}->{"StartOK"} ) {
		$ccmd{"oldstatuscode"} = $BigSister::common::status_codes{"green"};
		$ccmd{"oldstatus"} = "green";
	    }
		
	    event( \%ccmd );
	    $BigSister::common::current_status{$check} = {
		"status" => $ccmd{"statuscode"},
		"time" => $ccmd{"time"}
	    };
	}
	if( (! defined $BigSister::common::current_text{$check}) || ($BigSister::common::current_text{$check}->{"text"} ne $cmd{"text"}) ) {
	    my %ccmd = %cmd;
	    $ccmd{"cmd"} = "textchange";
	    $ccmd{"cmd_group"} = "textchange";
	    if( defined $BigSister::common::current_text{$check} ) {
		$ccmd{"oldtext"} = $BigSister::common::current_text{$check}->{"text"};
		$ccmd{"oldtime"} = $BigSister::common::current_text{$check}->{"time"};
		$cmd{"textsince"} = $ccmd{"textsince"} = $cmd{"time"};
	    }
	    event( \%ccmd );
	    $BigSister::common::current_text{$check} = {
		"text" => $ccmd{"text"},
		"time" => $ccmd{"time"}
	    };
	}
    }
    elsif( $cmd{"cmd"} eq "remove" ) {
	return unless( $cmd{"args"} =~ /^([^ ]+)\.([^ \.]+)$/ );
	$cmd{"host"} = $1;
	$cmd{"cmd_group"} = "status";
	$cmd{"check"} = $2;
	my $host = $1;
	my $expr = $2;
	$expr =~ s/\*/.*/;
	eval {
	    my $imatch = qr{^$expr$};
	    foreach my $i (grep($_ eq $host, keys %BigSister::common::current_status_c)) {
		foreach my $j (grep(m{$imatch}, keys %{$BigSister::common::current_status_c{$i}})) {
		    my $check=$i.".".$j;
		    delete $BigSister::common::current_status_c{$i}->{$j};
		    if (defined $BigSister::common::current_status{$check}) {
			my %ccmd = %cmd;
			$ccmd{"check"} = $j;
			$ccmd{"cmd"} = "remove";
			$ccmd{"cmd_group"} = "statuschange";
			$ccmd{"oldstatuscode"} = $BigSister::common::current_status{$check}->{"status"};
			$ccmd{"oldtime"} = $BigSister::common::current_status{$check}->{"time"};
			$ccmd{"oldstatus"} = $BigSister::common::status_texts{$ccmd{"oldstatuscode"}};
			event( \%ccmd );
			delete $BigSister::common::current_status{$check};
		    }
		    if (defined $BigSister::common::current_text{$check}) {
			my %ccmd = %cmd;
			$ccmd{"check"} = $j;
			$ccmd{"cmd"} = "remove";
			$ccmd{"cmd_group"} = "textchange";
			$ccmd{"oldtext"} = $BigSister::common::current_text{$check}->{"text"};
			$ccmd{"oldtime"} = $BigSister::common::current_text{$check}->{"time"};
			event( \%ccmd );
			delete $BigSister::common::current_text{$check};
		    }
		}
	    }
	};
	if( $@ ) {
	    BigSister::common::log( "remove expression '$expr' invalid: $@" );
 	}
    }
    elsif( $cmd{"cmd"} eq "displayname" ) {
	return unless( $cmd{"args"} =~ /^(.*?) (.*)$/ );
	$cmd{"group"} = $1;
	$cmd{"name"} = $2;
	$cmd{"cmd_group"} = "grouping";
    }
    elsif( ($cmd{"cmd"} eq "join") || ($cmd{"cmd"} eq "leave") ) {
	return unless( $cmd{"args"} =~ /^(.*?) (.*)$/ );
	$cmd{"host"} = $1;
	my @groups = split( " ", $2 );
	$cmd{"groups"} = \@groups;
	$cmd{"cmd_group"} = "grouping";
    }
    elsif( $cmd{"cmd"} eq "page" ) {
	$cmd{"cmd_group"} = "page";
    }
    elsif( $cmd{"cmd"} eq "savelogs" ) {
	$cmd{"cmd_group"} = "archiving";
	$cmd{"tag"} = $cmd{"args"};
    }
    elsif( $cmd{"cmd"} eq "sendlogs" ) {
	return unless( $cmd{"args"} );
	$cmd{"cmd_group"} = "archiving";
	$cmd{"tag"} = $cmd{"args"};
    }
    elsif( $cmd{"cmd"} eq "perf" ) {
        return unless( $cmd{"args"} =~ /^(\d+) (.*?):([^\s]+) (.+)$/ );
	$cmd{"perftime"} = $1;
	$cmd{"host"} = $2;
	$cmd{"variable"} = $3;
	$cmd{"value"} = $4;
	$cmd{"cmd_group"} = "perf";
    }
    elsif( $cmd{"cmd"} eq "ack" ) {
	return unless( $cmd{"args"} =~ /^([0-9a-z\-\:_,\.]+)\.([0-9a-z\-]+|\*) ([0-9:]+|forever) (.*?) (ack|del|ign|maint)\s*(.*)$/i );
	$cmd{"host"} = $1;
	$cmd{"check"} = $2;
	$cmd{"duration"} = $3;
	$cmd{"user"} = $4;
	$cmd{"mode"} = $5;
	$cmd{"comment"} = $6;
	$cmd{"cmd_group"} = "alarm_acking";
    }
    elsif( $cmd{"cmd"} eq "logrotate" ) {
	# for strange systems: close log file in order to 
	# give our data source an occasion for rotating logs
	close( $fh );
	undef $fopen;
	$rotatewait = 1;
    }

    event( \%cmd );
    return( $cmd{"time"} );

}


my %erred_time = ();

sub event {
    my( $cmd ) = @_;
    my $mask = $BigSister::common::cmd_groups{$cmd->{"cmd_group"}};
    my $monitor;
    my $time;

    print STDERR "event group ".($cmd->{"cmd_group"})." cmd ".($cmd->{"cmd"})."\n" if( $dl > 6 );
    my $profile = get_current_config()->{"options"}->{"Profile"};
    unless( defined $cmd->{"id"} ) {
	$cmd->{"id"} = $eventid++;
    }
    foreach $monitor( @monitors ) {
	if( $monitor->{"reqmask"} & $mask ) {
	    if( $profile ) {
		$time = MicroTime::gettimeofday();
	    }
	    eval {
		$monitor->event( $cmd );
	    };
	    if( $@ ) {
		unless( defined $erred_time{$monitor} 
		    && ( time - $erred_time{$monitor} < 600 ) ) {
		    $erred_time{$monitor} = time;
		    BigSister::common::log( "err", "module ".(ref($monitor))." failed with: $@" );
		}
	    }
	    if( $profile ) {
	        $time = MicroTime::difftime( $time );
		if( $time > 0.5 ) {
		    BigSister::common::log( "info", "profile: spent ".(int(time*100)/100)."s in $monitor, event $cmd->{'cmd'}" );
		}
  	    }
	}
    }
}


sub load_module {
    my( $module ) = @_;

    my $ret = 0;
    $module .= ".pm";
    eval {
	require "Statusmon/$module";
	$ret = 1;
    };
    unless( $ret ) {
	BigSister::common::log( "err", "module Statusmon::$module does not load: $@" );
    }
    return $ret;
}


sub read_config {
    my( $file ) = @_;
    my $cfg = new cfgfile( $file );
    my $count = 0;

    print STDERR "reading config file $file\n" if( $dl > 1 );
    $cfg->rewind();
    while( $_ = $cfg->nextline() ) {
	$count++;
	chomp;
	next if( /^$/ );
	my( $module, @cfg ) = BigSister::common::parse( $_ );
	print STDERR "setting up $module\n" if( $dl > 2 );
	next unless( load_module( $module ) );
	my $mon = "Statusmon::$module"->new();
	my $cfg;
	foreach $cfg ( @cfg ) {
	    if( $cfg =~ /^(.*?)=(.*)$/ ) {
		$mon->setarg( $1, $2 );
	    }
	    else {
		$mon->register( $cfg );
	    }
	}
	$mon->start();
	push( @monitors, $mon );
    }
    $acknowledge_provider = Statusmon::Statusmon::get_feature_provider( undef, "acknowledging" );
    return $count;
}


sub flush_status {
    my( $fh ) = new FileHandle;
    my( $key, $val, $db );

    open( $fh, ">$BigSister::common::fs{'var'}/bsmon.state" );
    foreach $db ( ["status", \%BigSister::common::current_status], ["text", \%BigSister::common::current_text], [ "checks", \%BigSister::common::current_status_c ] ) {
	while( ($key,$val) = each %{$db->[1]} ) {
	    print $fh $db->[0]." $key";
	    my( $attr, $cont );
	    while( ($attr, $cont) = each %$val ) {
		$cont =~ s/ /\xff/g;
		$cont =~ s/\n/\|\>/g;
		print $fh " $attr $cont";
	    }
	    print $fh "\n";
	}
    }
    close $fh;
}



sub read_status {
    my( $fh ) = new FileHandle;

    %BigSister::common::current_status = ();
    %BigSister::common::current_text = ();
    open( $fh, "<$BigSister::common::fs{'var'}/bsmon.state" );
    my $db = {
	"status" => \%BigSister::common::current_status,
	"text" => \%BigSister::common::current_text,
	"checks" => \%BigSister::common::current_status_c
    };
    while( <$fh> ) {
	chomp;
	if( /^(.*?) (.*?) (.*)$/ ) {
	    my ($what,$key,$val) = ($1,$2,$3);
	    my $hash = {};
	    my @cont = split( " ", $val );
	    while( @cont ) {
		my $attr = shift @cont;
		my $cont = shift @cont;
		$cont =~ s/\xff/ /g;
		$cont =~ s/\|\>/\n/g;
		$hash->{$attr} = $cont;
	    }
	    $db->{$what}->{$key} = $hash;
	}
    }
}


sub abort {
    my $monitor;

    foreach $monitor (@monitors) {
	$monitor->abort();
    }
}



sub get_current_config {
    my $provider = Statusmon::Statusmon::get_feature_provider( undef, "displayconfig" );
    return( {} ) if( ! defined $provider );
    return $provider->get_current_config();
}



sub sigdie {
    print STDERR "shutting down on signal ...\n" if( $BigSister::common::dl );
    flush_status();
    exit(0);
}

END {
   if( $$ == $BigSister::common::mainpid ) {
       flush_status();
   }
}

