#!/usr/bin/perl
#
# Copyright 2013 SPARTA, Inc.  All rights reserved.  See the COPYING
# file distributed with this software for details.

#
# owl-transfer-mgr					Owl Monitoring System
#
#       This script transfers Owl sensor data from the sensor to the manager.
#
# Revision History:
#	1.0	Initial version.				130116
#	2.0	Released as part of DNSSEC-Tools 2.0.		130301
#

use strict;

use Cwd;
use Getopt::Long qw(:config no_ignore_case_always);
use POSIX qw(setsid);
use Time::Local;
use Date::Format;

use FindBin;

use lib "$FindBin::Bin/../perllib";
use lib "$FindBin::Bin/../../common/perllib";
use owlutils;

#######################################################################
#
# Version information.
#
my $NAME   = 'owl-transfer-mgr';
my $VERS   = "$NAME version: 2.0.0";
my $DTVERS = 'DNSSEC-Tools version: 2.0';

###################################################
#
# Data required for command line options.
#
my %options = ();			# Filled option array.
my @opts =
(
	'config=s',			# Configuration file.
	'sensorsdir=s',			# Top-level sensors data directory.
	'logdir=s',			# Top-level logging directory.

	'foreground|fg',		# Run in foreground.
	'stop',				# Stop execution.

	'verbose',			# Verbose output.
	'Version',			# Version output.
	'help',				# Give a usage message and exit.
);


#
# Flag values for the various options.  Variable/option connection should
# be obvious.
#
my $verbose = 0;				# Display verbose output.
my $foreground;					# Foreground-execution flag.
my $stopper;					# Stop-execution flag.

###################################################
#
# Constants and variables for various files and directories.
#
my $basedir  = ".";				# Base directory we'll look in.

my $DEF_CONFIG	= $owlutils::DEF_CONFIG;	# Default config file nodename.
my $DEF_CONFDIR	= $owlutils::DEF_CONFDIR;	# Default config directory.
my $DEF_LOGDIR	= $owlutils::DEF_LOGDIR;	# Default log directory.
my $DEF_SENSORSDIR = $owlutils::DEF_SENSORSDIR;	# Default sensors data dir.

my $DEF_CONFFILE  = "$DEF_CONFDIR/$DEF_CONFIG";	# Default Owl config file.

###################################################

my $curdir;					# Current directory.
my $confdir    = $DEF_CONFDIR;			# Configuration directory.
my $conffile   = $DEF_CONFFILE;			# Configuration file.
my $sensorsdir;					# Sensors' data directory.
my $logdir;					# Logging directory.

#
# Every LOGCNT transfer cycles, owl-transfer-mgr will write a log message saying
# how many log attempts have been made.  If any failed, then the failure
# count will be reported.  A transfer cycle is one pass attempting to transfer
# to all ssh-users.  If owl-transfer-mgr attempts to transfer files once per
# minute, then a LOGCNT of 60 will result in a log message once per hour.
#
my $LOGCNT   = 60;


my $pidfile;					# Name of process-id file.
my @sshusers;					# user@host for rsyncing.
my $xfercnt = 0;				# Count of transfers.
my $xferint;					# Data-transfer interval.
my $xlog;					# Logging handle.

my $errs = 0;					# Error count.

#######################################################################

main();
exit(0);

#--------------------------------------------------------------------------
# Routine:	main()
#
sub main
{
	$| = 0;

	#
	# A little directory wiggling.
	#
	$curdir = getcwd();
	$basedir = $curdir if($basedir eq ".");

	#
	# Check our options.
	#
	doopts();

	#
	# Perform initialization steps.
	#
	startup();

	logger("$NAME starting");

	#
	# Grab some globals from the config file.
	#
        $pidfile  = $owlutils::pidfile;
	@sshusers = @owlutils::sshusers;
	$xferint = $owlutils::transfer_interval;

	if($verbose)
	{
		print "configuration parameters:\n";
		print "\tcurrent directory         \"$curdir\"\n";
		print "\tconfiguration file        \"$conffile\"\n";
		print "\tprocess-id file           \"$pidfile\"\n";
		print "\tsensors data directory    \"$sensorsdir\"\n";
		print "\tdata-transfer interval    \"$xferint\" minutes\n";
		print "\tssh users                 \"@sshusers\"\n";
		print "\n";
	}

	#
	# Don't proceed on errors.
	#
	if($errs)
	{
		my $sfx = ($errs != 1) ? 's' : '';	# Pluralization suffix.

		print "$NAME:  $errs error$sfx found during initialization; halting...\n";
		exit(1);
	}

	#
	# Daemonize ourself.
	# 
	exit(0) if((! $foreground) && fork());
	POSIX::setsid();
	owl_writepid();

	#
	# Periodically run the rsync to send the data to the manager.
	#
	teleporter();
}

#-----------------------------------------------------------------------------
# Routine:	doopts()
#
# Purpose:	This routine shakes and bakes our command line options.
#
sub doopts
{
	#
	# Parse the options.
	#
	GetOptions(\%options,@opts) || usage();

	#
	# Handle a few immediate flags.
	#
	version() if(defined($options{'Version'}));
	usage(1)  if(defined($options{'help'}));

	#
	# Set our option variables based on the parsed options.
	#
        $foreground = $options{'foreground'} || 0;
	$stopper    = $options{'stop'}	     || 0;
	$verbose    = $options{'verbose'}    || 0;

	#
	# Get the configuration file's name.  If the user specified one,
	# we'll use it.  If not, we'll try using one of two defaults.
	#
	if(defined($options{'config'}))
	{
		$conffile = $options{'config'};
	}
	else
	{
		$conffile = $DEF_CONFFILE;
		if(! -e $conffile)
		{
			$conffile = glob("$basedir/conf/owl.conf");
		}
	}

	#
	# Check our config and data directories.
	#
	$confdir    = $options{'confdir'}    if(defined($options{'confdir'}));
	$sensorsdir = $options{'sensorsdir'} if(defined($options{'sensorsdir'}));

}

#-----------------------------------------------------------------------------
# Routine:	startup()
#
# Purpose:	Do some initialization shtuff:
#			- set up Owl-specific fields
#			- set up signal handlers
#			- ensure we're the only owl-transfer-mgr running
#			- handle the -stop argument
#
sub startup
{
	#
	# Read the sensor configuration file.
	#
	owl_setup($NAME,$confdir,'',$logdir);
	if(owl_readconfig($conffile,'',$logdir) != 0)
	{
		exit(2);
	}

	#
	# Get the proper data directory name.
	#
	$confdir = setparam('confdir',$confdir,$owlutils::confdir,$DEF_CONFDIR);
	$logdir  = setparam('logdir',$logdir,$owlutils::logdir,$DEF_LOGDIR);
	$sensorsdir = setparam('sensorsdir',$sensorsdir,$owlutils::sensorsdir,$DEF_SENSORSDIR);

	exit(1) if(owl_chkdir('log', $logdir) == 1);
	exit(1) if(owl_chkdir('sensors-data', $sensorsdir) == 1);

	#
	# Set up our log file.
	#
	$xlog = owl_setlog($NAME,$logdir);

	#
	# Add the base directory if this isn't an absolute path.
	# We'll also handle a special case so we don't add the base
	# directory multiple times.
	#
	if($sensorsdir !~ /^\//)
	{
		if($sensorsdir =~ /^\.\//)
		{
			$sensorsdir =~ s/^../$curdir/;
		}

		if(($basedir ne ".") || ($sensorsdir !~ /^\.\//))
		{
			$sensorsdir = glob("$basedir/$sensorsdir");
		}
	}

	#
	# Set up our signal handlers.
	#
	$SIG{HUP}  = \&cleanup;
	$SIG{INT}  = \&cleanup;
	$SIG{QUIT} = \&cleanup;
	$SIG{TERM} = \&cleanup;
	$SIG{USR1} = 'IGNORE';
	$SIG{USR2} = 'IGNORE';

	#
	# If the user wants to shutdown any other owl-transfer-mgrs, we'll
	# send them SIGHUP and exit.
	#
	if($stopper)
	{
		owl_halt($NAME);
		exit(0);
	}

	#
	# Make sure we're the only owl-transfer-mgr running.  We'll also allow a
	# user to signal the other owl-transfer-mgr to shut down.
	#
	if(owl_singleton(0) == 0)
	{
		print STDERR "$NAME already running\n";
		exit(2);
	}

	#
	# Ensure the top-level sensors data directories is an absolute path.
	#
	$sensorsdir = "$curdir/$sensorsdir" if($sensorsdir !~ /^\//);

}

#------------------------------------------------------------------------
# Routine:	setparam()
#
# Purpose:	Figure out the value of a particular parameter, depending on
#		whether it was given as a command-line option or a config file
#		value.  It may be a default if none of the others was given.
#		The precedence (greatest to least) is:
#			command-line argument
#			configuration-file value
#			default
#
sub setparam
{
	my $str  = shift;			# Descriptive string.
	my $arg  = shift;			# Command line argument.
	my $cval = shift;			# Configuration file value.
	my $dval = shift;			# Default value.
	my $val;				# Value to use.

	$val = $dval;
	$val = $cval if(defined($cval));
	$val = $arg  if(defined($arg));

	return($val);
}

#-----------------------------------------------------------------------------
# Routine:	teleporter()
#
# Purpose:	This routine periodically transfers files from the sensor
#		to the Owl manager.
#
sub teleporter
{
	my $args;		# Arguments for rsync.
	my %errxfers = ();	# Count of each ssh-user's failed transfers.

	#
	# Set up our immutable command-line arguments.
	#
	$args = "--timeout=60 --partial --append --stats --recursive";

	print STDERR "$NAME starting\n";

	#
	# Forevermore we shall transfer files betwixt hither and yons.
	#
	while(42)
	{
		#
		# Go through the list of file destinations, and do an
		# rsync for each one.
		#
		foreach my $sshuser (@sshusers)
		{
			my @args;			# Separated arguments.
			my $sensor;			# Sensor portion.
			my $datadir;			# Sensor's data dir.
			my $rshargs;			# rsh-arguments portion.
			my $cmd;			# Command for rsync.
			my $out;			# Output from rsync.
			my $err;			# rsync return code.

			#
			# Set up our command line and arguments.  The sshuser
			# line has this format:
			#	user@host;ssh arguments
			# The ssh arguments are whatever is needed for
			# owl-transfer-mgr to ssh to this particular sensor.
			#
			@args = split /;/, $sshuser;
			$sensor = $args[0];
			$rshargs = "--rsh=\"ssh $args[1]\"";
			$cmd = "rsync -ar $rshargs $args";

			#
			# Build this sensor's data directory.
			#
			# Beware:  If this sshusers field contains spaces,
			# so with the directory name.
			#
			next if($args[2] eq '');
			$datadir = "$sensorsdir/$args[2]";

			#
			# Do the transfer for this sensor.
			#
			$out = `$cmd $sensor: $datadir`;
			$err = $? >> 8;

			#
			# Give info on this transfer, if it is desired.
			#
			if($verbose)
			{
				my $chronos;		# Timestamp.
				my $numstr;		# Files transferred.
				my $msg;		# Message string.

				#
				# Get timestamp for message.
				#
				$chronos = gmtime;
				chomp $chronos;

				#
				# Build the message.
				#
				$out =~ /(Number of files transferred:\s+\d+)\n/g;
				$numstr = $1;
				$msg = "$numstr \t$sensor";

				print "$chronos:  $msg\n" if($numstr ne '');
				logger($msg);
			}

			#
			# If the transfer failed, report an error and bump
			# the error-transfer count.
			#
			if($err)
			{
				my $msg = "problem transferring files, error return $err";
				print STDERR "$msg\n";
				logger($msg);
				$errxfers{$sshuser}++;
			}

			#
			# Write a periodic log message with the count of
			# transfers that have taken place.  This will show
			# the same number of transfers each time, so it's
			# more of an "I'm alive" message to the log file.
			#
			if($xfercnt == $LOGCNT)
			{
				my $msg;			# Message text.
				my $errs = $errxfers{$sshuser};	# Error count.

				#
				# Build the message.
				#
				$msg  = "$xfercnt transfer attempts to $sensor";
				$msg .= "; $errs failed" if($errs > 0);

				#
				# Write the message.
				#
				logger($msg);

				#
				# Reset for next round.
				#
				$xfercnt = 0;
				$errxfers{$sshuser} = 0;
			}
		}

		#
		# Bump our transfer count and wait for a bit.
		#
		$xfercnt++;
		sleep($xferint);
	}
}

#------------------------------------------------------------------------
# Routine:	cleanup()
#
# Purpose:	Close up shop.
#
sub cleanup
{
	print STDERR "$NAME shutting down\n";

	#
	# Remove the process-id file.
	#
	print STDERR "$NAME:  unlinking pidfile \"$pidfile\"\n" if($verbose);
	unlink($pidfile);

	exit(0);
}

#--------------------------------------------------------------------------
# Routine:	vprint()
#
sub vprint
{
	my $str = shift;

	print "$str" if($verbose);
}

#--------------------------------------------------------------------------
# Routine:	logger()
#
sub logger
{
	my $str = shift;
	my $outflag = shift;

	$xlog->log(level => 'info', message => $str);
#	vprint("$NAME:  $str\n") if($outflag);
}

#--------------------------------------------------------------------------
# Routine:	version()
#
sub version
{
	print STDERR "$VERS\n";
	print STDERR "$DTVERS\n";
	exit(0);
}

#--------------------------------------------------------------------------
# Routine:	usage()
#
sub usage
{
	print "$NAME [options]\n";
	print "\toptions:\n";
	print "\t\t-confdir directory\n";
	print "\t\t-config  file\n";
	print "\t\t-logdir  directory\n";
	print "\t\t-sensorsdir directory\n";

	print "\n";
	print "\t\t-foreground\n";
	print "\t\t-fg\n";
	print "\t\t-stop\n";

	print "\n";
	print "\t\t-verbose\n";
	print "\t\t-Version\n";
	print "\t\t-help\n";
	exit(0);
}

###########################################################################

=pod

=head1 NAME

owl-transfer-mgr - Transfers Owl sensor data to Owl manager

=head1 SYNOPSIS

  owl-transfer-mgr [options]

=head1 DESCRIPTION

B<owl-transfer-mgr> transfers Owl sensor data from an Owl sensor host to
its manager host.  B<owl-transfer-mgr> sets itself up as a daemon on the
Owl manager and periodically transfers new data files from the Owl sensor.

The Owl configuration file specifies the sensors that B<owl-transfer-mgr>.
will contact.  Only those sensors listed in a I<data ssh-user> line that
have the third subfield defined -- the sensor name subfield -- will be
contacted for data retrieval.  No other sensors will be contacted by
B<owl-transfer-mgr>.

The data-transfer operations are performed by B<rsync> over an B<ssh>
connection.  The efficiency of B<rsync> will minimize the impact on network
resources.  B<ssh> will protect the sensor data while it is in transit,
and it will also protect the Owl sensor from unauthorized access.

The B<owl.conf> configuration file defines the data that will be transferred,
the transfer destination, and the transfer frequency.  The following fields
are used:

	Configuration Field	    owl-transfer-mgr Use
	data sensors		    end location of transferred data
	data interval		    number of seconds between transfers
	remote ssh-user		    user@host for ssh/rsync use

The default configuration file is B<owl.conf> in the execution
directory.  If that file is not found, then B<conf/owl.conf> will be
used.  A configuration file may also be specified on the command line.

Since B<owl-transfer-mgr> is run on the manager, it is likely to be
retrieving data from multiple sensors.  All the data will be segregated
according to the source sensor, and each sensor's data will be stored in
a subdirectory beneath the I<sensors directory>.  This directory may be
specified with the B<-sensorsdir> commandline option or in the configuration
file.  If it is not given either way, then the default will be used.
The default sensors directory is specified in the B<owlutils.pm> module.

=head1 OPTIONS

B<owl-transfer-mgr> takes the following options:

=over 4

=item B<-confdir>

This option specifies the directory from which the Owl configuration data
file will be read.  The process-id file will also be stored here.

=item B<-config>

This option specifies the configuration file to use.

=item B<-fg>

=item B<-foreground>

This option causes B<owl-transfer-mgr> to run in the foreground, rather than
as a daemon.

=item B<-logdir>

This option specifies the directory for logging.

=item B<-sensorsdir>

This option specifies the directory to which sensor data will be
transferred from the Owl sensors.  This is a top-level directory
and the sensors' data directories will be beneath this directory.

=item B<-stop>

Stops the execution of an existing B<owl-transfer-mgr> process.

=item B<-verbose>

This option provides verbose output.

=item B<-Version>

This option provides the version of B<owl-transfer-mgr>.

=item B<-help>

This option displays a help message and exits.

=back

=head1 SEE ALSO

B<owl-dnstimer(1)>,
B<owl-transfer(1)>,
B<rsync(1)>,
B<ssh(1)>

B<owl-config(5)>

=head1 COPYRIGHT

Copyright 2013 SPARTA, Inc.  All rights reserved.

=head1 AUTHOR

Wayne Morrison, tewok@tislabs.com

=cut

