#!/usr/bin/perl
#
# Node Assassin - Installer
# Digimer; digimer@alteeve.com
# Apr. 14, 2010
# Version: 1.1.4
#
# Bugs;
# - None known, many expected
# 

# Play safe!
use strict;
use warnings;
use IO::Handle;
use File::Copy;

my $conf={
	'system'	=>	{
		conf_file	=>	"./etc/fence_na/fence_na.conf",
		'log'		=>	"./install.log",
		debug		=>	0,
		quiet		=>	0,
		yes		=>	0,
		'no'		=>	0,
	},
	install		=>	{
		directory	=>	{
			prefix		=>	"",
			agent		=>	"/sbin/",
			config		=>	"/etc/fence_na/",
			library		=>	"/etc/fence_na/",
			naos		=>	"/etc/fence_na/",
			INSTALL		=>	"/etc/fence_na/",
			CHANGES		=>	"/etc/fence_na/",
			README		=>	"/etc/fence_na/",
			fence_na_pod	=>	"/etc/fence_na/",
			man		=>	"/usr/share/man/man8/",
			cluster_ng	=>	"/usr/share/system-config-cluster/misc/",
		},
		files		=>	{
			agent		=>	"fence_na",
			config		=>	"fence_na.conf",
			library		=>	"fence_na.lib",
			naos		=>	"naos-1.1.4.4.c",
			INSTALL		=>	"INSTALL",
			CHANGES		=>	"CHANGES",
			README		=>	"README",
			fence_na_pod	=>	"fence_na.pod",
			man		=>	"fence_na.8.gz",
			cluster_ng	=>	"cluster.ng",
		},
	},
	source		=>	{
		files		=>	{
			agent		=>	"sbin/fence_na",
			config		=>	"etc/fence_na/fence_na.conf",
			library		=>	"etc/fence_na/fence_na.lib",
			naos		=>	"naos-1.1.4.4.c",
			build		=>	"na_build_v1.1.4.tar.gz",
			arduino		=>	"arduino-0018.tar.gz",
			INSTALL		=>	"INSTALL",
			CHANGES		=>	"CHANGES",
			README		=>	"README",
			fence_na_pod	=>	"fence_na.pod",
			man		=>	"usr/share/man/man8/fence_na.8.gz",
			cluster_ng	=>	"cluster.ng",
		},
	},
	inject		=>	{
		cluster_ng	=>	{
			agent		=>	q'
       <!-- Node Assassin -->
       <group>
        <attribute name="ipaddr"/>
        <optional>
        <attribute name="login"/>
        </optional>
        <optional>
        <attribute name="passwd"/>
        </optional>
        <optional>
        <attribute name="passwd_script"/>
        </optional>
        <optional>
         <attribute name="quiet"/>
        </optional>
       </group>
						',
			device		=>	q'
        <!-- Node Assassin -->
        <group>
         <attribute name="port"/>
         <attribute name="action"/>
        </group>
						',
		},
	},
};

# Make sure I am running as root.
# print "Real UID: [$<], Effective UID: [$>]\n";
die "The Node Assassin installer must be run as root. Exiting.\n" if (($< != 0) && ($> != 0));

# Log file for output.
my $log=IO::Handle->new();
open ($log, ">>$conf->{'system'}{'log'}") || die "Failed to open: [$conf->{'system'}{'log'}] for writing; Error: $!\n";

# This method can't pass in the '$log' handle, obviously, as it does not yet
# exist.
read_conf($conf);

# Set STDOUT and $log to hot (unbuffered) output.
if (1)
{
	select $log;
	$|=1;
	select STDOUT;
	$|=1;
}

# If this gets set in the next two function, the agent will exit.
my $bad=0;

# Read in arguments from the command line.
($bad)=read_cla($conf, $log, $bad);

record($conf, $log, "Ready to start the Node Assassin install:\n");

# Copy my source files.
foreach my $key (sort {$b cmp $a} keys %{$conf->{source}{files}})
{
	next if $key eq "cluster_ng";
	my $src=$conf->{source}{files}{$key};
	my $dst_dir=$conf->{install}{directory}{prefix}.$conf->{install}{directory}{$key};
	my $dst_file=$conf->{install}{files}{$key};
	copy_file($conf, $log, $src, $dst_dir, $dst_file);
}
# Merge in my Node Assassin data into the cluster.ng XML validation file.
merge_cluster_ng($conf, $log);

record($conf, $log, "\nInstallation completed!\n\n", 1);
record($conf, $log, "IMPORTANT! Please read!\n");
record($conf, $log, " - Before you can use Node Assassin, you must:\n");
record($conf, $log, " - Edit: [$conf->{install}{directory}{prefix}$conf->{install}{directory}{config}$conf->{install}{files}{config}]\n");
record($conf, $log, " - Edit and load: [./naos.1.1.4.4.c]\n");
record($conf, $log, "Read: 'INSTALL' for details on how to complete the above steps.\n\n");

exit(0);


###############################################################################
# Public Functions.                                                           #
###############################################################################

sub make_directory
{
	my ($conf, $log, $dir)=@_;
	
	if ( not -d $dir)
	{
		record($conf, $log, "I need to create: [$dir]\n");
		
		my @dirs=split/\//, $dir;
		my $rebuilt_dir="/";
		foreach my $dir (@dirs)
		{
			next if not $dir;
			$rebuilt_dir.="$dir/";
			if ( not -d $rebuilt_dir )
			{
				print "doesn't exist... creating.\n";
				my $ok=mkdir $rebuilt_dir, 0755 or record ($conf, $log, "Failed to create the directory: [$rebuilt_dir], error: $!\n", 1);
# 				my $ok=mkdir($rebuilt_dir, 0755) || die "Failed to create the directory: [$rebuilt_dir], error: $!\n";
				record($conf, $log, " - Created: [$rebuilt_dir]\n") if $ok;
			}
		}
	}
	
	return;
}

sub copy_file
{
	my ($conf, $log, $src, $dst_dir, $dst_file)=@_;
	
	make_directory($conf, $log, $dst_dir);
	my $dst=$dst_dir.$dst_file;
	record($conf, $log, "I will now copy: [$src] -> [$dst]\n");
	if ( -f $dst )
	{
		record($conf, $log, "WARNING! The destination file: [$dst] already exists.\n");
		record($conf, $log, "WARNING! If I proceed, it will be overwritten.\n");
		print "Proceed? [y/N] " if ((not $conf->{'system'}{'no'}) && (not $conf->{'system'}{yes}));
		if ($conf->{'system'}{'no'})
		{
			record($conf, $log, "\nAssume 'No' was set via command line switch, skipping.\n");
			return;
		}
		elsif ($conf->{'system'}{yes})
		{
			record($conf, $log, "\nAssume 'Yes' was set via command line switch, proceeding.\n");
		}
		else
		{
			my $answer=<STDIN>;
			chomp $answer;
			if ((lc($answer) ne "y") && (lc($answer) ne "yes"))
			{
				record($conf, $log, "Skipping.\n");
				return;
			}
		}
	}
	copy($src, $dst) or die "Failed to copy: [$src] -> [$dst], error: $!\n";
	record($conf, $log, " - Copied.\n");
	
	return;
}

sub merge_cluster_ng
{
	my ($conf, $log)=@_;
	
	# I must read in the existing file and inject the Node Assassin data in
	# order for the XML validation to work against the cluster.conf file.
	my $lines;
	my $src=$conf->{install}{directory}{cluster_ng}.$conf->{install}{files}{cluster_ng};
	my $dst_dir=$conf->{install}{directory}{prefix}.$conf->{install}{directory}{cluster_ng};
	make_directory($conf, $log, $dst_dir);
	my $dst=$dst_dir.$conf->{install}{files}{cluster_ng};
	
	# Read in the source file.
	record($conf, $log, "Reading: [$src]\n");
	my $read=IO::Handle->new();
	my $shell_call="<$src";
	open ($read, $shell_call) or die "Failed to read: [$src], error: $!\n";
	while (<$read>)
	{
		$lines.=$_;
	}
	$read->close();
	record($conf, $log, " - Read.\n");
	
	# Modify the source data to cleanly fit into the cluster.ng file.
	record($conf, $log, "Injecting NA support arguments.\n");
	$conf->{inject}{cluster_ng}{agent}=~s/\n\s+$//;
	$conf->{inject}{cluster_ng}{device}=~s/\n\s+$//;
	
	# Inject my data.
	$lines=~s/(\s+<attribute name="agent"\/>\s+<optional>\s+<choice>)/$1$conf->{inject}{cluster_ng}{agent}/;
	$lines=~s/(\s+<element name="device">\s+<attribute name="name">\s+<data type="IDREF"\/>\s+<\/attribute>\s+<choice>)/$1$conf->{inject}{cluster_ng}{device}/;
	record($conf, $log, " - Injected.\n");
	
	# Write it out.
	record($conf, $log, "Writing out: [$dst]\n");
	my $write=IO::Handle->new();
	$shell_call=">$dst";
	open ($write, $shell_call) or die "Failed to write: [$dst], error: $!\n";
	print $write $lines;
	$write->close();
	record($conf, $log, " - Written.\n");
	
	return;
}

# This cleanly exits the agent.
sub do_exit
{
	my ($conf, $log, $exit_status)=@_;
	$exit_status=9 if not defined $exit_status;
	
	# Close the Node Assassin and log file handle, if they exist.
	$log->close() if $log;
	
	exit ($exit_status);
}

# This returns the 'help' message.
sub help
{
	my ($conf, $log)=@_;
	
	# Point the user at the man page.
	print "\nPlease read the INSTALL file for info on installing the Node Assassin software.\n\n";
	
	do_exit($conf, $log, 0);
}

# Read in command line arguments
sub read_cla
{
	my ($conf, $log, $bad)=@_;
	
	# Loop through the passed arguments, if any.
	record($conf, $log, "Got args:\n") if $conf->{'system'}{debug};
	my $set_next="";
	foreach my $arg (@ARGV)
	{
		record($conf, $log, "[$arg]\n") if $conf->{'system'}{debug};
		
		# If 'set_next' has a value, push this argument into the 'conf'
		# hash.
		if ($set_next)
		{
			# It's set, use it's contents as the hash key.
			$conf->{install}{$set_next}=$arg;
			record($conf, $log, "Setting: 'install::$set_next': [$conf->{install}{$set_next}]\n") if $conf->{'system'}{debug};
			
			# Clear it now for the next go-round.
			$set_next="";
			next;
		}
		if ($arg=~/-h/)
		{
			# Print the help message and then exit.
			help($conf, $log);
		}
		elsif ($arg=~/-q/)
		{
			# Suppress all non-critical messages from STDOUT.
			$conf->{'system'}{quiet}=1;
		}
		elsif ($arg=~/-y/)
		{
			# Assume "yes" to all questions.
			$conf->{'system'}{yes}=1;
		}
		elsif ($arg=~/-n/)
		{
			# Assume "no" to all questions.
			$conf->{'system'}{'no'}=1;
		}
		elsif ($arg=~/-directory=(.*?)/)
		{
			$conf->{install}{directory}=$1;
		}
		elsif ($arg=~/^-/)
		{
			$arg=~s/^-//;
			
			if ($arg eq "d")
			{
				# This is the IP address or hostname of the
				# Node Assassin to call.
				$set_next="directory";
				record($conf, $log, "Next argument will be stored in: [install::$set_next]\n") if $conf->{'system'}{debug};
			}
		}
		else
		{
			### MADI: I might want to pick up arguments via multiple lines.
			# Bad argument.
			record($conf, $log, "\nERROR: Argument: [$arg] is not valid!\n");
			record($conf, $log, "ERROR: Please run './fence_na --help' to see a list of valid arguments.\n\n");
			$bad=1;
		}
	}
}

# Read in the config file.
sub read_conf
{
	my ($conf)=@_;
	$conf={} if not $conf;
	
	# I can't call the 'record' method here because I've not read in the
	# log file and thus don't know where to write the log to yet. Comment
	# out or delete 'print' statements before release.
	my $read=IO::Handle->new();
	my $shell_call="$conf->{'system'}{conf_file}";
	open ($read, "<$shell_call") or die "Failed to read: [$shell_call], error was: $!\n";
	# To see this log message, debug will need to be set true in the
	# initialization hash.
	record($conf, $log, "Shell call: [$shell_call]\n") if $conf->{'system'}{debug};
	while (<$read>)
	{
		chomp;
		my $line=$_;
		next if not $line;
		next if $line !~ /=/;
		$line=~s/^\s+//;
		$line=~s/\s+$//;
		next if $line =~ /^#/;
		next if not $line;
		my ($var, $val)=(split/=/, $line, 2);
		$var=~s/^\s+//;
		$var=~s/\s+$//;
		$val=~s/^\s+//;
		$val=~s/\s+$//;
		next if (not $var);
		record($conf, $log, "Storing: [$var] = [$val]\n") if $conf->{'system'}{debug};
		_make_hash_reference($conf, $var, $val);
	}
	$read->close();
	
	return (0);
}

# This function simply prints messages to both the log and to stdout.
sub record
{
	my ($conf, $log, $msg, $critical)=@_;
	$critical=0 if not $critical;
	
	# The log file gets everything.
	print $log $msg;
	print $msg if not $conf->{'system'}{quiet};
	
	# Critical messages have to print, so this ensure that it gets out
	# when 'quiet' is in use.
	print $msg if (($critical) && ($conf->{'system'}{quiet}));
	
	return(0);
}

###############################################################################
# Private functions                                                           #
###############################################################################

### Contributed by Shaun Fryer and Viktor Pavlenko by way of TPM.
# This is a helper to the above '_add_href' method. It is called each time a
# new string is to be created as a new hash key in the passed hash reference.
sub _add_hash_reference
{
	my $href1=shift;
	my $href2=shift;
	
	for my $key (keys %$href2)
	{
		if (ref $href1->{$key} eq 'HASH')
		{
			_add_hash_reference($href1->{$key}, $href2->{$key});
		}
		else
		{
			$href1->{$key}=$href2->{$key};
		}
	}
}

### Contributed by Shaun Fryer and Viktor Pavlenko by way of TPM.
# This takes a string with double-colon seperators and divides on those
# double-colons to create a hash reference where each element is a hash key.
sub _make_hash_reference
{
	my $href=shift;
	my $key_string=shift;
	my $value=shift;
# 	print "variable: [$key_string], value: [$value]\n";
	
	my $chomp_root=0;
	if ($chomp_root) { $key_string=~s/\w+:://; }
	
	my @keys = split /::/, $key_string;
	my $last_key = pop @keys;
	my $_href = {};
	$_href->{$last_key}=$value;
	while (my $key = pop @keys)
	{
		my $elem = {};
		$elem->{$key} = $_href;
		$_href = $elem;
	}
	_add_hash_reference($href, $_href);
}
