#!/usr/bin/perl -w
#
#  "SystemImager"
#
#  Copyright (C) 2005 Andrea Righi <a.righi@cineca.it>
#  Copyright (C) 2005-2006 Bernard Li <bernard@vanhpc.org>

require 5.006;

use lib "/usr/lib/systemimager/perl";
use strict;
use Fcntl ':flock';
use POSIX qw(setsid);
use threads;
use threads::shared;
use Thread::Queue;
use IO::Socket;
use IO::Select;
use XML::Simple;
use Storable qw(freeze thaw);
use Getopt::Long;
use SystemImager::Options;
use SystemImager::Config;
use vars qw($config $VERSION);
use constant DEFAULT_PORT => 8181;

my $VERSION = "4.3.0";
my $program_name = "si_monitor";
my $version_info = << "EOF";
$program_name (part of SystemImager) v$VERSION

Copyright (C) 1999-2001 Andrea Righi <a.righi\@cineca.it>
Copyright (C) 2005-2006 Bernard Li <bernard\@vanhpc.org>

This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF

my $get_help = "\n       Try \"--help\" for more options.";

my $help_info = $version_info . <<"EOF";

Usage: $program_name [OPTION]...

Options: (options can be presented in any order and may be abbreviated)
 --help                 Display this output.

 --version              Display version and copyright information.

 --db DATABASENAME      Where DATABASENAME is the name of the file
                        where clients informations will be stored.
                        (All the data in this file will be stored in
                        XML format).

 --port PORT            The port used by $program_name for listening
                        client connections (the default port is 8181).

 --log LOGFILE          If specified every log information will be
                        reported in the file LOGFILE.

 --log_level 1|2|3      Define the verbosity of the logging:
                            1 = errors only (low verbosity);
                            2 = errors and warnings;
                            3 = errors, warnings and all the debug
                                messages (high verbosity).

Download, report bugs, and make suggestions at:
http://systemimager.org/
EOF

my $CONFDIR = '/etc/systemimager';

# load resources
my %conf;
my $conffile = "$CONFDIR/$program_name";
if (-r $conffile) {
    Config::Simple->import_from($conffile, \%conf);
}

# Lock directory is needed.
$conf{'lock_dir'} ||= "/var/lock/systemimager";
if (! -d $conf{'lock_dir'}) {
    mkdir($conf{'lock_dir'}, 0700) or
        die "error: couldn't create lock directory: '$conf{'lock_dir'}'\n";
}

# Get the database file.
$conf{'monitor_db'} ||= "/var/lib/systemimager/clients.xml";

my ($help, $version, $quiet, $port, $database, $log_file, $log_level);
GetOptions(
	"help"		=> \$help,
	"version"	=> \$version,
	"quiet"		=> \$quiet,
	"port=s"	=> \$port,
	"db=s"		=> \$database,
	"log=s"		=> \$log_file,
	"log_level=i"	=> \$log_level,
) or die "$help_info";

### BEGIN evaluate commad line options ###
if ($help) {
	print "$help_info";
	exit(0);
}

if ($version) {
	print "$version_info";
	exit(0);
}

# Create the database if it doesn't exist.
$database = $conf{'monitor_db'};
unless (-f $database) {
	open(DB, '>', "$database") or 
	die "error: cannot open file \"$database\" for writing!\n";
	close(DB);
}

# Get the port to listen. 
unless ($port) {
	$port = DEFAULT_PORT;
}

# Evaluate the logging level.
if (defined($log_level)) {
	if (($log_level < 1) or ($log_level > 3)) {
		print "$program_name: $log_level: not a valid log level!\n";
		print "$help_info";
		exit(1);
	}
} else {
	# Use the default log level.
	$log_level = 2;
}

# Get monitor log file.
unless ($log_file) {
	# Do not log anything.
	$log_file = '/dev/null';
	if ($log_level) {
		print "warning: log file was not specified: ignoring log level: $log_level.\n";
		$log_level = '';
	}
}
### END evaluate command line options ###

# Daemon stuff.
my $pid_file = "/var/run/si_monitor.pid";
chdir '/' or die "error: can't chdir to /: $!\n";
open(STDIN, '/dev/null') or die "error: can't read from /dev/null: $!\n";
open(STDOUT, '>>' . $log_file) or die "error: can't write to $log_file: $!\n";
open(STDERR, '>>' . $log_file) or die "error: can't write to $log_file: $!\n";
umask(0);

# Autoflush on.
select(STDOUT);
$| = 1;

# Reaping zombie child processes.
$SIG{CHLD} = 'IGNORE';

# Create a new process session.
setsid or die localtime() . ": error: can't start a new session: $!\n";

# Create the pid file.
open(FILE, ">$pid_file") or
	die localtime() . ": error: cannot open file: $pid_file\n";
print FILE "$$\n";
close(FILE);

# Define lock files.
my $lock_file = $conf{'lock_dir'} . "/db.si_monitor.lock";

# Remove old locks.
unlink("$lock_file");

# Initialize global (frozen) XML structure.
my $xml_data : shared;
my $xml_db = load_db();
if ($xml_db) {
	my $xml = XMLin($xml_db, KeyAttr => {client => 'name'}, ForceArray => 1);
	$xml_data = freeze($xml);
}

# Data queue to wake-up and shutdown the XML loader thread.
my $wakeup_sync_thread = Thread::Queue->new();

# Spawn the sync thread.
async {
	while (1) {
		my $xml_frozen = get_xml_data();
		if ($xml_frozen) {
			my $xml = thaw($xml_frozen);
			if ($xml) {
				my $xml_raw = XMLout($xml);
				sync_db($xml_raw);
				# Do not sync more than once in 10sec, otherwise
				# we could hog the machine...
				sleep(10);
			}
		}
		$wakeup_sync_thread->dequeue();
	}
}->detach();

# Open a TCP socket.
my $server = new IO::Socket::INET(
			Proto           => 'tcp',
			Type		=> SOCK_STREAM,
			Listen          => SOMAXCONN,
			LocalPort       => $port,
			ReuseAddr       => 1,
		) or
	die(localtime() .
		": error: couldn't create a TCP socket to listen on port $port: $!\n");

# Monitor daemon initialized.
if ($log_level > 2) {
	print localtime() .
	      ": $program_name daemon is listening on port $port\n";
}

# Begin to accept client connections.
my $events = new IO::Select();
while (1) {
	my $client = $server->accept();
	unless ($client) {
		print localtime() .
			": error: couldn't accept client connection!\n";
		next;
	}
	# Get the other peer identity.
	my $clientaddr = $client->peerhost();
	unless ($clientaddr) {
		# Refuse the client connection.
		print localtime() . 
			": warning: could not identify other end of a client request, ignoring.\n"
			if ($log_level > 1);
		close($client);
		next;
	}
	# TODO allow only authorized hosts... -AR-
	if ($log_level > 2) {
		# Report info in the log.
		my $clientport = $client->peerport();
		print localtime() .
		      ": connection accepted for $clientaddr:$clientport.\n";
	}
	# Get the client request (set a 30sec timeout to wait the info sent by
	# the client).
	$events->add($client);
	if ($events->can_read(30)) {
		$_ = <$client>;
		if ($_) {
			chomp;
			# Update the database.
			update_db($clientaddr, $_);
		}
	} else {
		print localtime() .
		      ": warning: connection from $clientaddr timed out.\n";
	}
	# Close client socket;
	$events->remove($client);
	$client->close();
}

# Usage:
# $data = get_xml_data();
# Description:
#	Get shared XML structure safely (in mutual exclusion).
sub get_xml_data
{
	lock($xml_data);
	return $xml_data;
}

# Usage:
# put_xml_data($data);
# Description:
#	Store shared XML structure safely (in mutual exclusion).
sub put_xml_data
{
	my $data = shift;
	lock($xml_data);
	$xml_data = $data;
}

# Usage:
# $data = load_db();
# Description:
#	Load the XML structure from file.
sub load_db
{
	my $db;

	open(LOCK, ">", "$lock_file") or 
		die localtime() . ":error: cannot open lock file \"$lock_file\"!\n";
	flock(LOCK, LOCK_SH);
	
	# Open database in mutual exclusion.
	open(DB, '<', $database) or 
		die localtime() . ": error: cannot open \"$database\" for reading!\n";
	# Parse XML database.
	if (-s $database) {
		foreach (<DB>) {
			$db .= $_;
		}
	}
	close(DB);

	flock(LOCK, LOCK_UN);
	close(LOCK);

	return $db;
}

# Usage:
# sync_db($data);
# Description:
#	Synchronize the XML structure in the permanent file.
sub sync_db
{
	my ($out) = @_;

	return unless ($out);

	open(LOCK, ">", "$lock_file") or 
		die localtime() . ":error: cannot open lock file \"$lock_file\"!\n";
	flock(LOCK, LOCK_EX);

	open(DB, '>', $database) or
		die localtime() . ": error: cannot open \"$database\" for writing!\n";
	
	# Sync the database.
	print DB $out; 

	# Close and unlock database.
	close(DB);
	flock(LOCK, LOCK_UN);
	close(LOCK);
}

# Usage:
# update_db($hostname, $client_request);
# Description:
#	Update the database according to the client request.
#
# XXX: update_db() is not ran in mutual exclusion! This is not needed
# because only update_db() can write the $xml_data shared structure and
# only one instance of update_db can exist at a generic time (the socket
# accepts only one client per time). If that implementation will be
# changed remember to fix also this routine. -AR-
#
sub update_db
{
	my ($host, $request) = @_;
	my ($client, $mac, $timestamp);

	if ($log_level > 2) {
		# Report the request in the log.
		print localtime() . ": $host request -> $_\n";
	}

	# Parse the client request.
	my @args = split(/:/, $request);
	foreach (@args) {
		if (s/^mac=//) {
			$mac = $_;
		} elsif (s/^ip=//) {
			$client->{'ip'} = $_;
		} elsif (s/^host=//) {
			$client->{'host'} = $_;
		} elsif (s/^cpu=//) {
			$client->{'cpu'} = $_;
		} elsif (s/^ncpus=//) {
			$client->{'ncpus'} = int($_);
		} elsif (s/^kernel=//) {
			$client->{'kernel'} = $_;
		} elsif (s/^mem=//) {
			$client->{'mem'} = int($_ / 1024);
		} elsif (s/^os=//) {
			$client->{'os'} = $_;
		} elsif (s/^tmpfs=//) {
			$client->{'tmpfs'} = $_;
		} elsif (s/^time=//) {
			$client->{'time'} = int($_ / 60);
		} elsif (s/^status=//) {
			$client->{'status'} = $_;
		} elsif (s/^speed=//) {
			$client->{'speed'} = int($_);
		} elsif (s/^first_timestamp=//) {
			$client->{'first_timestamp'} = $_;
		}
	}
        $timestamp = time();

	# Check if mac address has been specified.
	unless (defined($mac)) {
		if ($log_level > 1) {
			print localtime() .
				": warning: bad request from $host (mac address not specified)!\n";
		}
		return;
	}
	
	# Update the clients table in mutual exclusion.
	my $xml;
	my $xml_frozen = get_xml_data();
	if ($xml_frozen) {
		$xml = thaw($xml_frozen);
	}

	$xml->{'client'}->{$mac}->{'ip'} = $client->{'ip'}
		if $client->{'ip'};
	$xml->{'client'}->{$mac}->{'host'} = $client->{'host'}
		if $client->{'host'};
	$xml->{'client'}->{$mac}->{'cpu'} = $client->{'cpu'}
		if $client->{'cpu'};
	$xml->{'client'}->{$mac}->{'ncpus'} = $client->{'ncpus'}
		if $client->{'ncpus'};
	$xml->{'client'}->{$mac}->{'kernel'} = $client->{'kernel'}
		if $client->{'kernel'};
	$xml->{'client'}->{$mac}->{'mem'} = $client->{'mem'}
		if defined($client->{'mem'});
	$xml->{'client'}->{$mac}->{'os'} = $client->{'os'}
		if $client->{'os'};
	$xml->{'client'}->{$mac}->{'tmpfs'} = $client->{'tmpfs'}
		if $client->{'tmpfs'};
	$xml->{'client'}->{$mac}->{'time'} = $client->{'time'}
		if defined($client->{'time'});
	$xml->{'client'}->{$mac}->{'status'} = $client->{'status'}
		if defined($client->{'status'});
	$xml->{'client'}->{$mac}->{'speed'} = $client->{'speed'}
		if defined($client->{'speed'});
	$xml->{'client'}->{$mac}->{'log'} = $client->{'log'}
		if $client->{'log'};

	# Add the server timestamps.
	$xml->{'client'}->{$mac}->{'timestamp'} = $timestamp;
	$xml->{'client'}->{$mac}->{'first_timestamp'} = $timestamp
		if $client->{'first_timestamp'};

	$xml_frozen = freeze($xml);
	put_xml_data($xml_frozen);

	# Wake-up the sync thread.
	if (!$wakeup_sync_thread->pending()) {
		$wakeup_sync_thread->enqueue(undef);
	}
}

__END__

=head1 NAME

si_monitor - systemimager real time monitoring daemon

=head1 SYNOPSIS

si_monitor [OPTIONS]...

=head1 DESCRIPTION

B<si_monitor> is a tool to perform real-time monitoring
of the clients installation status.
It listen to a specific port and collects informations
periodically sent by clients using plain TCP/IP connections.

Clients must have defined the MONITOR_SERVER (and optional
MONITOR_PORT) as boot parameters to enable the monitoring feature.

All these informations are stored in a XML database. The database
can be defined with the options B<--db DATABASENAME>.

For default the file B</var/lib/systemimager/clients.xml> is taken.

=head1 OPTIONS

=over 8

=item B<--help>

Display a short help.

=item B<--version>

Display version and copyright information.

=item B<--db DATABASENAME>

An XML file to store all the informations collected by the
si_monitor daemon. If you run this command for the first time
the file will be initialized to an empty file.

=item B<--port PORT>

The port used by the si_monitor daemon for listening clients
connections.

The default port is 8181.

=item B<--log LOGFILE>

If this option is used every kind of information will be
reported in the file LOGFILE.
This option can be useful to debug clients connections.

=item B<--log_level 1|2|3>

There are 3 levels of logging. In order of verbosity they are:
  B<1>) Error:    serious problem. Execution cannot or should
               not continue;
  B<2>) Warining: there is an unexpected condition. Processing
               can continue usually correctly, but may result
               in other problems during the future execution;
  B<3>) Debug:    verbose output. May be used to trace execution
               or get hints about precursors to problems.

=head1 SEE ALSO

systemimager(8), si_monitortk(1)

=head1 AUTHOR

Andrea Righi <a.righi@cineca.it>.

=head1 COPYRIGHT AND LICENSE

Copyright 2003 by Andrea Righi <a.righi@cineca.it>.

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.

=cut

