#!/usr/bin/perl
##################################################################################
# Pandora Data Server
##################################################################################
# Copyright (c) 2004-2006 Sancho Lerena, slerena@gmail.com
# Copyright (c) 2005-2006 Artica Soluciones Tecnologicas S.L
#
# 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
##################################################################################

# Includes list
use strict;
use warnings;

use XML::Simple;                	# Useful XML functions
use Digest::MD5;                	# MD5 generation
use Time::Local;                	# DateTime basic manipulation
use DBI;                               	# DB interface with MySQL
use Date::Manip;                	# Needed to manipulate DateTime formats of input, output and compare
use File::Copy;                          # Needed to manipulate files
use threads;
use threads::shared;

# Librerias / Modulos de pandora
use pandora_config;
use pandora_tools;
use pandora_db;

# FLUSH in each IO, only for DEBUG, very slow !
$| = 1;

my %pa_config; 

# Inicio del bucle principal de programa
pandora_init(\%pa_config,"Pandora Server");

# Read config file for Global variables
pandora_loadconfig (\%pa_config,0);

# Audit server starting
pandora_audit (\%pa_config, "Pandora Daemon starting", "SYSTEM", "System");

# BE CAREFUL, if you daemonize, you need to launch threads BEFORE daemonizing.
if ($pa_config{"daemon"} eq "1" ){
	&daemonize;
}

# KeepAlive checks for Agents, only for master servers, in separate thread
threads->new( \&pandora_keepalived, \%pa_config);

# Module processor subsystem
pandora_dataserver(\%pa_config);


#------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------
#---------------------  Main Perl Code below this line-------------------------------
#------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------
#------------------------------------------------------------------------------------




##############################################################################
# Main loop
##############################################################################

sub pandora_dataserver {
	my $pa_config = $_[0];
	my $file_data;
	my $file_md5;
	my @file_list;
	my $onefile; # Each item of incoming directory 
	my $agent_filename;
	my $dbh = DBI->connect("DBI:mysql:pandora:$pa_config->{'dbhost'}:3306",$pa_config->{"dbuser"}, $pa_config->{"dbpass"},{ RaiseError => 1, AutoCommit => 1 });

	while ( 1 ) { # Pandora module processor main loop
		opendir(DIR, $pa_config->{'incomingdir'} ) or die "[FATAL] Cannot open Incoming data directory at $pa_config->{'incomingdir'}: $!";
 		while (defined($onefile = readdir(DIR))){
   			push @file_list,$onefile; 	# Push in a stack all directory entries for this loop
 		}
        	while (defined($onefile = pop @file_list)) {	# Begin to process files
			threads->yield;
                	$file_data = "$pa_config->{'incomingdir'}/$onefile";
                	next if $onefile =~ /^\.\.?$/;     # Skip . and .. directory
                	if ( $onefile =~ /([\-\:\;\.\,\_\s\a\*\=\(\)a-zA-Z0-9]*).data\z/ ) {  # First filter any file that doesnt like ".data"
   				$agent_filename = $1;
   				$file_md5 = "$pa_config->{'incomingdir'}/$agent_filename.checksum";
				if (( -e $file_md5 ) or ($pa_config->{'pandora_check'} == 0)){ # If check is disabled, ignore if file_md5 exists
    					# Comprobamos integridad
    					my $check_result;
					$check_result = md5check ($file_data,$file_md5);
					if (($pa_config->{'pandora_check'} == 0) || ($check_result == 1)){
						# PERL cannot "free" memory on user demmand, so 
						# we are declaring $config hash reference in inner loop
						# to force PERL system to realloc memory in each loop.
						# In Pandora 1.1 in "standard" PERL Implementations, we could
						# have a memory leak problem. This is solved now :-)
						# Source : http://www.rocketaware.com/perl/perlfaq3/
                                 		# Procesa_Datos its the main function to process datafile
						my $config; # Hash Reference, used to store XML data
                                        	# But first we needed to verify integrity of data file
                                        	if ($pa_config->{'pandora_check'} == 1){
							logger ($pa_config, "Integrity of Datafile using MD5 is verified: $file_data",3);
						}
     						eval { # XML Processing error catching procedure. Critical due XML was no validated
                                  			logger ($pa_config, "Ready to parse $file_data",4);
                                  			$config = XMLin($file_data, forcearray=>'module');
                          			};
                          			if ($@) {
                                   			logger ($pa_config, "[ERROR] Error processing XML contents in $file_data",0);
                                   			copy ($file_data,$file_data."_BADXML");
                                   			if (($pa_config->{'pandora_check'} == 1) && ( -e $file_md5 )) {
								copy ($file_md5,$file_md5."_BADCHECKUM");
							}
                          			}
						procesa_datos($pa_config, $config, $dbh); 
						undef $config;
                                        	# If _everything_ its ok..
						# delete files
                                        	unlink ($file_data);
                                        	if ( -e $file_md5 ) {
							unlink ($file_md5);
						}
                                	} else { # md5 check fails
     						logger ( $pa_config, "[ERROR] MD5 Checksum failed! for $file_data",0);
						# delete files
                                        	unlink ($file_data);
                                        	if ( -e $file_md5 ) {
							unlink ($file_md5);
						}
    					}
   				} # No existe fichero de checksum, ignoramos el archivo
                	}
        	}
        	closedir(DIR);
		threads->yield;
        	sleep $pa_config->{"server_threshold"};
	}
} # End of main loop function

#################################################################################
## SUB pandora_keepalived
## Pandora Keepalive alert daemon subsystem
##################################################################################

sub pandora_keepalived {
	my $pa_config = $_[0];
	my $dbh = DBI->connect("DBI:mysql:pandora:$pa_config->{'dbhost'}:3306",$pa_config->{"dbuser"}, $pa_config->{"dbpass"},{ RaiseError => 1, AutoCommit => 1 });
	while ( 1 ){
		sleep $pa_config->{"server_threshold"};
		threads->yield;
		keep_alive_check($pa_config,$dbh);
		pandora_serverkeepaliver($pa_config,0,$dbh); # 0 for dataserver
	}
}


#################################################################################
## SUB keep_alive_check  ()
## Calculate a global keep alive check for agents without data and an alert defined 
##################################################################################

sub keep_alive_check {
        # Buscamos si existe una alerta definida para cada item de la tablacombinacion agente/modulo
	my $pa_config = $_[0];
	my $dbh = $_[1];
    	
    	my $query_idag = "select * from talerta_agente_modulo";
    	my $s_idag = $dbh->prepare($query_idag);
        $s_idag ->execute;
        my @data; my $err; my $flag;
	
	if ($s_idag->rows != 0) {
	while (@data = $s_idag->fetchrow_array()) {
		threads->yield;
		my $id_aam = $data[0];
		my $id_alerta = $data[2];
		my $id_agente_modulo = $data[1];
		# Only checks keep_alive special modules (-1 on type)
		if (dame_id_tipo_modulo($pa_config, $id_agente_modulo, $dbh) == -1) {
			my $campo1 = $data[3];
			my $campo2 = $data[4];
			my $campo3 = $data[5];
			my $dis_max = $data[7];
			my $dis_min = $data[8];
			my $threshold = $data[9];
			my $last_fired = $data[10];
			my $max_alerts = $data[11];
			my $times_fired = $data[12];
			my $alert_fired = 0;
			my $fecha_ultima_alerta = ParseDate($last_fired);
			my $ahora_mysql = &UnixDate("today","%Y-%m-%d %H:%M:%S");
			my $timestamp = $ahora_mysql;
			my $fecha_actual = ParseDate( $ahora_mysql );
			# If we need to update MYSQL last_fired will use $ahora_mysql

			# Calculate if INTERVAL x2 for this agent is bigger than sub last contact date with actual date
			my $nombre_agente = dame_nombreagente_agentemodulo($pa_config, $id_agente_modulo, $dbh);
			my $id_agente = dame_agente_id($pa_config, $nombre_agente, $dbh);
			if (dame_desactivado($pa_config, $id_agente, $dbh) == 0){
				my $fecha_ultimocontacto = dame_ultimo_contacto($pa_config,$id_agente,$dbh);
				my $intervalo = dame_intervalo($pa_config, $id_agente, $dbh); # Seconds
				my $intervalo_2 = $intervalo*2;
				$fecha_ultimocontacto = ParseDate($fecha_ultimocontacto);
				my $fecha_limite = DateCalc($fecha_ultimocontacto,"+ $intervalo_2 seconds",\$err);
				$flag = Date_Cmp($fecha_actual,$fecha_limite);
				if ( $flag >= 0) { $alert_fired = 1 } else { $alert_fired=0 } ;
				if (( $flag >= 0 ) && ($max_alerts >= $times_fired)){ # Calculate if max_alerts for this time is exhausted
					# Alert Trigger is ON !
					# Check if alert is fired by event-success
					my $time_threshold = $threshold; # from defined alert 
					$fecha_limite = DateCalc($fecha_ultima_alerta,"+ $time_threshold seconds",\$err);
					$flag = Date_Cmp($fecha_actual,$fecha_limite);
					if ( $flag >= 0 ) {
						# Alert Trigger is fired by time-threshold
						# Get "command" string from Alert Definition in DB
						my $comando = dame_comando_alerta($pa_config, $id_alerta, $dbh);
						$times_fired = $times_fired + 1;
						$query_idag = "update talerta_agente_modulo set times_fired = $times_fired, last_fired = '$ahora_mysql' where id_aam = $id_aam ";
						my $s3_idag = $dbh->prepare($query_idag);
						$s3_idag ->execute;
						$s3_idag->finish();
						my $nombre_agente = dame_nombreagente_agentemodulo($pa_config,$id_agente_modulo,$dbh);
						logger($pa_config, "Alert (KeepAlive) TRIGGERED for $nombre_agente ! ",1);
						my $id_grupo = dame_grupo_agente($pa_config,$id_agente,$dbh);
						my $descripcion = "Agent down";
						pandora_event($pa_config, $descripcion,$id_grupo,$id_agente,$dbh);
						if ($id_alerta > 0){ # id_alerta 0 is reserved for internal audit system
							$comando =~ s/_field1_/"$campo1"/gi;
							$comando =~ s/_field2_/"$campo2"/gi;
							$comando =~ s/_field3_/"$campo3"/gi;
							$comando =~ s/_agent_/$nombre_agente/gi;
							$comando =~ s/_timestamp_/$timestamp/gi;
							$comando =~ s/\^M/\r\n/g; # Replace Carriage rerturn and line feed
							# Clean up some "tricky" characters
							$comando =~ s/>/>/g;
							# EXECUTING COMMAND !!!
							eval {
								my $exit_value = system ($comando);
								$exit_value  = $? >> 8; # Shift 8 bits to get a "classic" errorlevel
								if ($exit_value != 0) {
									logger( $pa_config,"Executed command for triggered alert had errors (errorlevel =! 0) ",0);
								}
							};
							if ($@){
								logger($pa_config, "ERROR: Error executing alert command  ( $comando )",1);
								logger($pa_config, "ERROR Code: $@",2);
							}
						} else { # id_alerta = 0, is a internal system audit
								logger($pa_config, "Internal audit lauch for agent name $nombre_agente",2);
								$campo1 =~ s/_timestamp_/$timestamp/g;
								pandora_audit ($pa_config, $campo1, $nombre_agente, "User Alert",$dbh);
						}
					} # if $flag >=0 (for time-firing calculation)
					} elsif (($alert_fired == 0) && ($times_fired != 0)){ # If alert doesnt fired and fired counter isnt zero, lets reset counter
						$query_idag = "update talerta_agente_modulo set times_fired = 0 where id_aam = $id_aam ";
						my $s3_idag = $dbh->prepare($query_idag);
						$s3_idag ->execute;
						$s3_idag ->finish();
						my $id_grupo = dame_grupo_agente($pa_config, $id_agente, $dbh);
						my $descripcion = "Agent up";
						pandora_event($pa_config, $descripcion,$id_grupo,$id_agente, $dbh);
					} # if $flag >= 0 (for time-threshold)
				} # Disabled agent
			} #if (dame_id_tipo_modulo($id_agente_modulo) == -1)
		} # While
	} # if ($s_idag->rows != 0) 
	$s_idag->finish();
}

#################################################################################
## SUB procesa_datos (par1)
## Procesa un paquete de datos (XML preprocesado)
##################################################################################
## param_1 : Nombre de la estructura contenedora de datos (XML)

sub procesa_datos {
   	my $pa_config = $_[0];
    	my $datos = $_[1]; 
	my $dbh = $_[2];

	my $tipo_modulo; my $agent_name; 
	my $timestamp; my $interval; 
	my $os_version; my $agent_version;
    	my $id_agente; my $module_name;
	$agent_name = $datos->{'agent_name'};
	$timestamp = $datos->{'timestamp'};
	$agent_version = $datos->{'version'};
	$interval = $datos->{'interval'};
	$os_version = $datos->{'os_version'};
	# Check for parameteres, not all version agentes gives the same parameters ! 
	if (length($interval) == 0){ $interval = -1; } # No update for interval !      
	if (length($os_version) == 0){ $os_version = "N/A"; } # No update for interval ! 
	if (defined $agent_name){
		$id_agente = dame_agente_id($pa_config,$agent_name,$dbh);
		if ($id_agente > 0) {
			pandora_lastagentcontact($pa_config, $timestamp, $agent_name, $os_version, $agent_version, $interval, $dbh);
			foreach my $part(@{$datos->{module}}) {
				$tipo_modulo = $part->{type}->[0];
				$module_name = $part->{name}->[0];
				logger($pa_config, "Processing packet Name ( ".$module_name." ) type ( $tipo_modulo ) for agent ( $agent_name )",5);
				if ($tipo_modulo eq 'generic_data') {
					module_generic_data($pa_config, $part,$timestamp,$agent_name,"generic_data", $dbh);
				}
				elsif ($tipo_modulo eq 'generic_data_inc') {
					module_generic_data_inc($pa_config, $part,$timestamp,$agent_name,"generic_data_inc", $dbh);
				}
				elsif ($tipo_modulo eq 'generic_data_string') {
					module_generic_data_string($pa_config,$part,$timestamp,$agent_name,"generic_data_string", $dbh);
				}
				elsif ($tipo_modulo eq 'generic_proc') {
					module_generic_proc($pa_config,$part,$timestamp,$agent_name,"generic_proc", $dbh);
				}
				else {
					logger($pa_config,"ERROR: Received data from an unknown module ($tipo_modulo)",2);
				}
			}
		} else {
			logger($pa_config,"ERROR: There is no agent defined with name $agent_name ($id_agente)",2);
		}
	} else {
		logger($pa_config,"ERROR: Received data from an unnamed agent",1);
	}
}