#!/usr/local/bin/perl -w
#------------------------------------------------------------------
# 
# Karma Copyright (C) 1999  Sean Hull <shull@pobox.com>
#
#   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., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
#
#
#------------------------------------------------------------------
#
# karmagentd
#
# This script runs on the server machine (of each database you're
# monitoring, to get status of the OS (load average, % idle)
# and populate karma_os_stats with this information.  It also
# gathers information about any new ORA- errors in the alert.log
# and populates the karma_alertlog_errors table with it.
#
# Notes:  You don't HAVE to use this daemon at all.  If you don't,
# the OS and alert log columns will not be displayed in karma.
#
# You need to run one of these daemons for each instance which you
# want to gather these stats on (for now).  Sorry.
#
# Needs to run as the Oracle user...
#-----------------------------------------------------------------



#----------------------------------------------
# 
# PROTOTYPES
#
#----------------------------------------------
sub checkLine ($);
sub readPosition ($);
sub writePosition ($$);
sub debugPrint ($$);
sub printHelp ();

use Getopt::Std;
use karma;
use strict;

BEGIN {
    unless (eval "require DBI") {
	print 
	    "You must have DBI installed to use karma.\n",
	    "Please install it first, and try again.\n";
	exit;
    }
}

BEGIN {
    unless (eval "require DBD::Oracle") {
	print
	    "You must have DBD::Oracle installed to use karma.\n",
	    "Please install it first, and try again.\n";
	exit;
    }
}


use IO::File;
require 5.004;

#
# needs windows support
#
if ($main::WINDOWS == 0) {
    $main::CMD_UPTIME = "/usr/bin/uptime";
}

#
# get the command line options
#
$main::opt_h = undef;
$main::opt_t = undef;
$main::opt_u = undef;
$main::opt_f = undef;
$main::opt_r = undef;
$main::opt_j = undef;
$main::opt_p = undef;
$main::opt_k = undef;
$main::opt_s = undef;
$main::opt_b = undef;
$main::opt_h = undef;
$main::opt_d = undef;
$main::opt_w = undef;
$main::opt_v = undef;
$main::opt_l = undef;
getopts('ht:u:f:a:rp:j:s:b:h:d:k:vwl:');

if ($main::opt_v) {
    printVersion ();
}
if ($main::opt_w) {
    printWarranty ();
}
if ($main::opt_h) {
    printHelp ();
}

if ($main::opt_d) {
    $main::DEBUG_LEVEL = $main::opt_d;
}

if ($main::opt_s) {
    $ENV{ORACLE_SID} = $main::opt_s;
}
if ($main::opt_b) {
    $ENV{ORACLE_BASE} = $main::opt_b;
}
if ($main::opt_h) {
    $ENV{ORACLE_HOME} = $main::opt_h;
}
if ($main::opt_r) {
    reset_alertlog ();
}

$main::TNS = "";
if ($main::opt_t) {
    $main::TNS = $main::opt_t;
} elsif (defined ($ENV{DBI_DSN})) {
    $main::TNS = $ENV{DBI_DSN};
}
#
# default to the OFA location
#
$main::ALERTLOG_FILE = "$ENV{ORACLE_BASE}$main::PATH_DELIM" . 'admin' . "$main::PATH_DELIM$ENV{ORACLE_SID}" . "$main::PATH_DELIM" . 'bdump' . "$main::PATH_DELIM" . "alert_$ENV{ORACLE_SID}.log";
if ($main::opt_a) {
    $main::ALERTLOG_FILE = $main::opt_a;
}
$main::PASS = "manager";
if ($main::opt_p) {
    $main::PASS = $main::opt_p;
} elsif (defined ($ENV{DBI_PASS})) {
    $main::PASS = $ENV{DBI_PASS};
} else {
    print ("Password: ");
    $main::PASS = <STDIN>;
    chomp ($main::PASS);
}

$main::USER = "karma";
if ($main::opt_u) {
    $main::USER = $main::opt_u;
} elsif (defined ($ENV{DBI_USER})) {
    $main::USER = $ENV{DBI_USER};
}
my $FREQUENCY = 60;
if ($main::opt_f) {
    $FREQUENCY = $main::opt_f * 60;
}
my $POSITION_FILE = ".karmagent.sav";
if ($main::opt_k) {
    $POSITION_FILE = $main::opt_k;
}

#
# set the logfile name
#
$main::logfile_name = "-";
if (defined ($main::opt_l)) {
    $main::logfile_name = "$main::opt_l";
}


$main::logfile = new IO::File ">>$main::logfile_name";
if (not (defined $main::logfile)) {
    $main::logfile = new IO::File ">>-";
    logMessage ("Cannot open logfile: $main::logfile_name, using STDOUT");

}

#
# background ourselves
#
daemonize ();


#
# signal handlers...
#
$SIG{TERM} = \&catch_term;

$main::cALERT_POSITION = "alert_position";
$main::cBEGINNING_OF_FILE = 0;

@main::uptimes = ();
#$main::UPCMD = "uptime";
$main::uptimeString = "";

print ("\nStarting karma monitoring daemon...\n");
debugMessage ("DEBUG_LEVEL:$main::DEBUG_LEVEL\n", undef);
debugMessage ("-- Environment Settings --\n", undef);
debugMessage ("ORACLE_BASE:$ENV{ORACLE_BASE}\n", undef);
debugMessage ("ORACLE_HOME:$ENV{ORACLE_HOME}\n", undef);
debugMessage ("ORACLE_SID:$ENV{ORACLE_SID}\n", undef);


#
# database handle
#
debugMessage ("Connecting - TNS:$main::TNS USER:$main::USER PASS:$main::PASS\n", 2);
$main::dbh = DBI->connect ("DBI:Oracle:$main::TNS", $main::USER, $main::PASS);

if ($main::dbh) {
    logMessage ("Successfully connected to $main::USER\@$main::TNS...\n");
} else {
    logMessage ("Failed to connect to $main::USER\@$main::TNS - Exiting.\n");
}

# 
# statements for inserting data into the db
#
$main::uptimeStatement =
    "INSERT INTO karma_os_stats VALUES (sysdate, ?, ?, ?, 0)";
$main::alertlogStatement =
    "INSERT INTO karma_alertlog_errors VALUES (sysdate, ?, ?, ?)";

#
# prepare the statements
#
$main::uptimeSth = $main::dbh->prepare ($main::uptimeStatement);
$main::alertlogSth = $main::dbh->prepare ($main::alertlogStatement);

$main::prevTime = getDayMinutes (time ());
$main::currTime = 0;

$main::currLine = "";
$main::currPosition = 0;
if ($main::opt_j) {
    $main::currPosition = $main::opt_j;
} else {
    $main::currPosition = readPosition ($POSITION_FILE);
}

debugMessage ("Reading alertlog: $main::ALERTLOG_FILE\n", undef);
debugMessage ("Start reading at byte $main::currPosition\n", undef);
$main::alert_file = new IO::File "<$main::ALERTLOG_FILE";
if (not ($main::alert_file)) {
    logMessage ("Could not open alertlog file.  Exiting.\n");
    exit;
}

seek ($main::alert_file, $main::currPosition, $main::cBEGINNING_OF_FILE);

while (1) {
#    if (($main::currTime == 0) || ($main::currTime >= $main::prevTime + $FREQUENCY)) {

	$main::prevTime = $main::currTime;

	# 
	# fetch the most current uptime stats
	#
	$main::uptimeString = `$main::CMD_UPTIME`;
	chop ($main::uptimeString);
	$main::uptimeString =~ s/^.*://;
	@main::uptimes = split (',', $main::uptimeString);
	debugMessage ("INSERTING OS VALUES: $main::uptimes[0], $main::uptimes[1], $main::uptimes[2]\n", 2);
	$main::uptimeSth->execute ($main::uptimes[0], $main::uptimes[1], $main::uptimes[2]);

	#
	# fetch any new alert log errors
	#
	while (<$main::alert_file>) {
	    
	    $main::currLine = $_;
	    chomp ($main::currLine);
	    $main::lineNum++;	
	    if ($main::lineNum % 50) {
		debugMessage ("LINE:$main::lineNum\n", 2);
	    }
	    $main::currPosition = tell;

	    debugMessage ("TESTING - $main::currLine\n", 2);
	    $main::lineRef = checkLine ($main::currLine);
	    
	    if ($main::lineRef->[0]) {
		debugMessage ("INSERTING ALERTLOG VALS: $main::lineRef->[0], $main::lineRef->[1], $main::lineRef->[2]\n", 2);
		$main::alertlogSth->execute ($main::lineRef->[0],
				       $main::lineRef->[1],
				       $main::lineRef->[2]);

	    }
	    
	}

	writePosition ($POSITION_FILE, $main::currPosition);

	debugMessage ("Sleeping for $FREQUENCY seconds...\n", undef);
	sleep ($FREQUENCY);

    $main::currTime = getDayMinutes (time ());
}




#-----------------------------------------------------------------------
#
# returns the time of day in minutes
#
#-----------------------------------------------------------------------
sub printHelp () {

    print 
	("\n",
	 " v - print version info and exit\n",
	 " h - print this help info\n",
	 " f - fequency in minutes to wakeup & check things (default 1)\n",
	 " r - reset the alert.log, and truncate it's table\n",
	 " u - user to login as (default karma)\n",
	 " p - oracle login password (otherwise you're prompted)\n",
	 " j - jump j bytes in file (takes precedence over save file)\n",
	 " t - tnsname of the database to watch (default local)\n",
	 " a - specify alert.log file (default OFA)\n",
	 " k - use this file to store seek position\n",
	 " b - specify ORACLE_BASE (takes precedence over env)\n",
	 " h - specify ORACLE_HOME (takes precedence over env)\n",
	 " s - specify ORACLE_SID (takes precedence over env)\n",
	 " d - debug level (default 0, no debugging)\n",
	 " w - print the warranty and exit\n",
	 " l - specify logfile to write messages to \n",
	 "\n",
	 "$0 [-h] [-f \#] [-r] [-u karma] [-p pass] [-j \#] [-t DB]\n",
	 "\t[-a alert.log] [-k karmagent.sav] [-b ORACLE_BASE]\n",
	 "\t[-h ORACLE_HOME] [-s ORACLE_SID] [-d \#]\n");
    
    exit;

}


#--------------------------------------------------------------
#
# checkLine
#
#--------------------------------------------------------------
sub checkLine ($) {
    my ($inLine) = @_;

    my $facility = "";
    my $errNum = 0;
    my $errText = "";
    my $noErrors = 0;

    if ($inLine =~ /^ORA-/) {

	debugMessage ("ERRORS --- $inLine\n", undef);
	$facility = "ORA";
	$errNum = $inLine;
	$errNum =~ s/^ORA-//;
	$errNum =~ s/\s.*$//;
	$errText = $inLine;
	$errText =~ s/^ORA-//;
	$errText =~ s/^\d*\s//;
	$noErrors = 0;

	return [$facility, $errNum, $errText];
    }
    
    return [];
}

#--------------------------------------------------------------
#
#
#
#--------------------------------------------------------------
sub readPosition ($) {
    my ($inFile) = @_;

    my $currLine = "";
    my $retPos = 1;

    
    my $pos_file = new IO::File "<$inFile";
    if ($pos_file) {
	while (<$pos_file>) {
	    $currLine = $_;
	    chomp $currLine;
	    if ($currLine =~ /$main::cALERT_POSITION/) {
		$currLine =~ s/^.*$main::cALERT_POSITION://;
		$currLine =~ s/\D*//;
		$retPos = $currLine;
	    }
	}
	
	$pos_file->close;
    } else {
	logMessage ("Can't open position file: $!\n");
    }
    
    return $retPos;
}

#--------------------------------------------------------------
#
#
#
#--------------------------------------------------------------
sub writePosition ($$) {
    my ($inFile, $inPos) = @_;

    my $pos_file = new IO::File ">$inFile";
    if (defined $pos_file) {
	print $pos_file ("$main::cALERT_POSITION:$inPos\n");
	$pos_file->close; 
    } else {
	logMessage ("Cannot write to position file.  $!\nPosition will not be saved.\n");
    }

}

#--------------------------------------------------------------
#
#
#
#--------------------------------------------------------------
sub reset_alertlog () {

}

#--------------------------------------------------------------
#
# normal kill
#
#--------------------------------------------------------------
sub catch_term {
    writePosition ($POSITION_FILE, $main::currPosition);
    
    $main::logfile->close;
    #close (AFILE);
    #
    # cleanup and exit
    #
    if ($main::uptimeSth) {
	$main::uptimeSth->finish;
    }
    if ($main::alertlogSth) {
	$main::alertlogSth->finish;
    }
    if ($main::dbh) {
	$main::dbh->disconnect;
    }
    exit 1;

}




#---------------------------------
#
# Plain Old Documentation (pod...)
#
#---------------------------------

=pod

=head1 NAME

Karma - karmagentd

=head1 DESCRIPTION

The karmagentd daemon is an additional utility which is used
in conjunction with OS and/or alertlog monitoring.  In order
for karmad to find valid information regarding either of these
services, this daemon must be running on each target database.

=head1 SYNOPSIS

The karmagentd daemon checks statistics from the operating
system and inserts them into a table called karma_os_stats
in the target database.  If this table is missing, 
karmagentd will continue running supporting alertlog
monitoring, if possible.

The karmagentd daemon's more important functionality is in
monitoring your alertlog on designated destination databases.
It does this by reading through the file on startup, and
then waking up periodically to check for changes.  If it finds
any 'ORA-' errors, it will insert the timestamp, and error
along with the associated text into the 
karma_alertlog_errors table.  If this table doesn't exist,
and karmagentd failed to do gather OS statistics, it will
exit with a message.  Additionally, karmagentd attempts to
use the .karmagent.sav file to store the current seek point
into the alertlog file, so it can start from where it left off.

=head1 NOTES

The basic way to invoke karmagentd is as follows:

C<$ ./karmagentd -u karma -t mydb>

Invoked this way, without specifying the password on the
command line is best.  If you choose to specify the password
on the command line, it can be viewed via the operating
system command 'ps', or equivalent on Win32 platforms, which
presents a rather large security hole, and is not recommended.
Instead, invoke as described above, and karmagentd will
simply prompt you for the password.

Optionally, the alertlog file can be specified explicitely
C<-a /opt/oracle/admin/mydb/bdump/alertmydb.log>, however if
it is in the standard OFA location, karmagentd should find
it.

=cut

