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

# Play safe!
use strict;
use warnings;
use IO::Handle;
use File::Copy;
require "./installer.lib";

my $conf={
	'system'	=>	{
		conf_file	=>	"./etc/fence_na/fence_na.conf",
		'log'		=>	"./install.log",
		debug		=>	0,
		quiet		=>	0,
		yes		=>	0,
		'no'		=>	0,
		os_name		=>	"",
		os_ver		=>	"",
	},
	install		=>	{
		centos		=>	{
			directory	=>	{
				prefix		=>	"",
				agent		=>	"/sbin/",
				config		=>	"/etc/fence_na/",
				library		=>	"/etc/fence_na/",
				naos		=>	"/etc/fence_na/",
				build		=>	"/etc/fence_na/",
				arduino		=>	"/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",
				build		=>	"na_build_v1.1.4.1.tar.gz",
				arduino		=>	"arduino-0018.tar.gz",
				INSTALL		=>	"INSTALL",
				CHANGES		=>	"CHANGES",
				README		=>	"README",
				fence_na_pod	=>	"fence_na.pod",
				man		=>	"fence_na.8.gz",
				cluster_ng	=>	"cluster.ng",
			},
		},
		fedora		=>	{
			directory	=>	{
				prefix		=>	"",
				agent		=>	"/usr/sbin/",
# 				stonith		=>	"/usr/lib64/stonith/plugins/external/",
				config		=>	"/etc/fence_na/",
				library		=>	"/etc/fence_na/",
				naos		=>	"/etc/fence_na/",
				build		=>	"/etc/fence_na/",
				arduino		=>	"/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_rng	=>	"/usr/share/cluster/",
			},
			files		=>	{
				agent		=>	"fence_na",
# 				stonith		=>	"node_assassin",
				config		=>	"fence_na.conf",
				library		=>	"fence_na.lib",
				naos		=>	"naos-1.1.4.4.c",
				build		=>	"na_build_v1.1.4.1.tar.gz",
				arduino		=>	"arduino-0018.tar.gz",
				INSTALL		=>	"INSTALL",
				CHANGES		=>	"CHANGES",
				README		=>	"README",
				fence_na_pod	=>	"fence_na.pod",
				man		=>	"fence_na.8.gz",
				cluster_rng	=>	"cluster.rng",
			},
		},
	},
	source		=>	{
		centos		=>	{
			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.1.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",
			},
		},
		fedora		=>	{
			files		=>	{
				agent		=>	"sbin/fence_na",
# 				stonith		=>	"usr/lib64/stonith/plugins/external/",
				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.1.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_rng	=>	"cluster.rng",
			},
		},
	},
	inject		=>	{
		centos		=>	{
			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>
							',
			},
		},
		fedora		=>	{
			cluster_rng	=>	{
				agent		=>	q`
      <!-- The fence_na agent entry was added by the Node Assassin installer. -->
      <!-- fence_na -->
      <group>
        <optional>
          <attribute name="option"/> <!-- deprecated; for compatibility.  use "action" -->
        </optional>
        <optional>
          <attribute name="ipaddr" rha:description="Node Assassin IP or name to talk to" />
        </optional>
        <optional>
          <attribute name="passwd" rha:description="Password needed to access the Node Assassin(s)" />
        </optional>
        <optional>
          <attribute name="passwd_script" rha:description="Script to retrieve password (not implemented)" />
        </optional>
        <optional>
          <attribute name="login" rha:description="Login name used to log into the Node Assassin(s)" />
        </optional>
        <optional>
          <attribute name="action" rha:description="Operation to perform. Valid operations: on, off, reboot, status, list, release, boot or shutdown" />
        </optional>
        <optional>
          <attribute name="quiet" rha:description="Supress output" />
        </optional>
      </group>
      <!-- Please do not delete the following line. It is needed by the uninstaller. -->
      <!-- end fence_na -->
							`,
			},
		},
	},
};

# 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'}") or 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, $log);

# 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);

# Detect the operating system.
detect_os($conf, $log);

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

if ( $conf->{'system'}{os_name} eq "centos" )
{
	install_centos($conf, $log);
}
elsif ( $conf->{'system'}{os_name} eq "fedora" )
{
	install_fedora($conf, $log);
}

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

exit(0);


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

# This installs Node Assassin on Fedora
sub install_fedora
{
	my ($conf, $log)=@_;
	
	# Make sure I'm on version 13.
	if ($conf->{'system'}{os_ver} !~/^13/)
	{
		(my $major_ver)=($conf->{'system'}{os_ver} =~/^(\d+)/);
		record($conf, $log, __LINE__, "\nWARNING: Your version of Fedora has not been tested!\n", 1);
		if ($major_ver < 13)
		{
			record($conf, $log, __LINE__, "WARNING: Version 12 and under may not work.\n", 1);
		}
		else
		{
			record($conf, $log, __LINE__, "WARNING: Version 14 and higher may work if not too much has changed.\n", 1);
		}
		record($conf, $log, __LINE__, "WARNING: THE AGENT MAY FAIL! Be sure to test the agent if you proceed!\n", 1);
		record($conf, $log, __LINE__, "Do you wish to proceed anyway?\n[y/N] ", 1);
		my $answer=<STDIN>;
		chomp $answer;
		if ((lc($answer) ne "y") && (lc($answer) ne "yes"))
		{
			record($conf, $log, __LINE__, "Aborted installation, Exiting.\n");
			exit (1);
		}
	}
	
	# Copy my source files.
	my $os=$conf->{'system'}{os_name};
	foreach my $key (sort {$b cmp $a} keys %{$conf->{source}{$os}{files}})
	{
		next if $key eq "cluster_rng";
		my $src=$conf->{source}{$os}{files}{$key};
		my $dst_dir=$conf->{install}{$os}{directory}{prefix}.$conf->{install}{$os}{directory}{$key};
		my $dst_file=$conf->{install}{$os}{files}{$key};
# 		print "Copy: [$src] -> [$dst_dir$dst_file]\n";
		copy_file($conf, $log, $src, $dst_dir, $dst_file);
		if ($dst_dir =~ /\/fence_na$/)
		{
			record($conf, $log, __LINE__, "Setting: [${dst_dir}${dst_file}] to be executable.\n");
			my $mode=0755;
			chmod $mode, $dst_dir.$dst_file or record($conf, $log, __LINE__, "Failed to set the file: [${dst_dir}${dst_file}] to be executable. Error was: $!\n", 2);
		}
	}
	# Merge in my Node Assassin data into the cluster.rng XML validation file.
	merge_cluster_rng($conf, $log);
	
	return (1);
}

# This installs Node Assassin on CentOS
sub install_centos
{
	my ($conf, $log)=@_;
	
	# Make sure I'm on version 5.
	if ($conf->{'system'}{os_ver} !~/^5/)
	{
		(my $major_ver)=($conf->{'system'}{os_ver} =~/^(\d+)/);
		record($conf, $log, __LINE__, "\nWARNING: Your version of CentOS has not been tested!\n", 1);
		if ($major_ver < 5)
		{
			record($conf, $log, __LINE__, "WARNING: Version 4 and under are most likely not going to work.\n", 1);
		}
		else
		{
			record($conf, $log, __LINE__, "WARNING: Version 6 and higher may work if the old cluster stack has been installed.\n", 1);
		}
		record($conf, $log, __LINE__, "THE AGENT MAY FAIL! Be sure to test the agent if you proceed!\n", 1);
		record($conf, $log, __LINE__, "Do you wish to proceed anyway?\n[y/N] ", 1);
		my $answer=<STDIN>;
		chomp $answer;
		if ((lc($answer) ne "y") && (lc($answer) ne "yes"))
		{
			record($conf, $log, __LINE__, "Aborted installation, Exiting.\n");
			exit (1);
		}
	}
	
	# Copy my source files.
	my $os=$conf->{'system'}{os_name};
	foreach my $key (sort {$b cmp $a} keys %{$conf->{source}{$os}{files}})
	{
		next if $key eq "cluster_ng";
		my $src=$conf->{source}{$os}{files}{$key};
		my $dst_dir=$conf->{install}{$os}{directory}{prefix}.$conf->{install}{$os}{directory}{$key};
		my $dst_file=$conf->{install}{$os}{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);
	
	return (1);
}

# This merges the XML into cluster.ng to add support for Node Assassin in the
# xmlint tests.
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 $os=$conf->{'system'}{os_name};
	my $src=$conf->{install}{$os}{directory}{cluster_ng}.$conf->{install}{$os}{files}{cluster_ng};
	my $dst_dir=$conf->{install}{$os}{directory}{prefix}.$conf->{install}{$os}{directory}{cluster_ng};
	make_directory($conf, $log, $dst_dir);
	my $dst=$dst_dir.$conf->{install}{$os}{files}{cluster_ng};
	
	# Before anything, make a backup of the source file.
	record($conf, $log, __LINE__, "Backing up the original 'cluster.ng' file.\n", 1);
	copy_file($conf, $log, $src, $conf->{install}{$os}{directory}{cluster_ng}, "$conf->{install}{$os}{files}{cluster_ng}.pre-node_assassin");
	
	# Read in the source file.
	record($conf, $log, __LINE__, "Reading: [$src]");
	my $read=IO::Handle->new();
	my $shell_call="<$src";
	open ($read, $shell_call) or record($conf, $log, __LINE__, "Failed to read: [$src], error: $!\n", 2);
	my $skip=0;
	while (<$read>)
	{
		$lines.=$_;
		$skip=1 if $_ =~ "<!-- Node Assassin -->";
		last if $skip;
	}
	$read->close();
	record($conf, $log, __LINE__, "\n - Node Assassin support already added to cluster.rng, skipping.\n", 1) if $skip;
	return if $skip;
	
	# Proceed with injection.
	record($conf, $log, __LINE__, " - Read.\n");
	
	# Modify the source data to cleanly fit into the cluster.ng file.
	record($conf, $log, __LINE__, "Injecting NA support arguments.");
	$conf->{inject}{$os}{cluster_ng}{agent}=~s/\n\s+$//;
	$conf->{inject}{$os}{cluster_ng}{device}=~s/\n\s+$//;
	
	# Inject my data.
	$lines=~s/(\s+<attribute name="agent"\/>\s+<optional>\s+<choice>)/$1$conf->{inject}{$os}{cluster_ng}{agent}/;
	$lines=~s/(\s+<element name="device">\s+<attribute name="name">\s+<data type="IDREF"\/>\s+<\/attribute>\s+<choice>)/$1$conf->{inject}{$os}{cluster_ng}{device}/;
	record($conf, $log, __LINE__, " - Injected.\n");
	
	# Write it out.
	record($conf, $log, __LINE__, "Writing out: [$dst]");
	my $write=IO::Handle->new();
	$shell_call=">$dst";
	open ($write, $shell_call) or record($conf, $log, __LINE__, "\nFailed to write: [$dst], error: $!\n", 2);
	print $write $lines;
	$write->close();
	record($conf, $log, __LINE__, " - Written.\n");
	
	return;
}

# This merges the XML into cluster.rng to add support for Node Assassin in the
# xmlint tests. This is the Cluster3 type.
sub merge_cluster_rng
{
	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 $os=$conf->{'system'}{os_name};
	my $src=$conf->{install}{$os}{directory}{cluster_rng}.$conf->{install}{$os}{files}{cluster_rng};
	my $dst_dir=$conf->{install}{$os}{directory}{prefix}.$conf->{install}{$os}{directory}{cluster_rng};
	make_directory($conf, $log, $dst_dir);
	my $dst=$dst_dir.$conf->{install}{$os}{files}{cluster_rng};
	
	# Before anything, make a backup of the source file.
	record($conf, $log, __LINE__, "Backing up the original 'cluster.rng' file.\n", 1);
	copy_file($conf, $log, $src, $conf->{install}{$os}{directory}{cluster_rng}, "$conf->{install}{$os}{files}{cluster_rng}.pre-node_assassin");
	
	# Read in the source file.
	record($conf, $log, __LINE__, "Reading: [$src]\n");
	my $read=IO::Handle->new();
	my $shell_call="<$src";
	open ($read, $shell_call) or record($conf, $log, __LINE__, "Failed to read: [$src], error: $!\n", 2);
	my $skip=0;
	while (<$read>)
	{
		$lines.=$_;
		$skip=1 if $_ =~ "<!-- fence_na -->";
		last if $skip;
	}
	$read->close();
	record($conf, $log, __LINE__, " - Node Assassin support already added to cluster.rng, skipping.\n", 1) if $skip;
	return if $skip;
	
	# Proceed with injection.
	record($conf, $log, __LINE__, " - Read.\n");
	
	# Modify the source data to cleanly fit into the cluster.rng file.
	record($conf, $log, __LINE__, "Injecting NA support arguments.\n");
	$conf->{inject}{$os}{cluster_rng}{agent}=~s/\n\s+$//;
	
	# Inject my data.
	$lines=~s/(\s+<!-- begin specific fence devices -->)/$1\n$conf->{inject}{$os}{cluster_rng}{agent}/;
	record($conf, $log, __LINE__, " - Injected.\n");
	
	# Write it out.
	record($conf, $log, __LINE__, "Writing out: [$dst]\n");
	my $write=IO::Handle->new();
	$shell_call=">$dst";
	open ($write, $shell_call) or record($conf, $log, __LINE__, "Failed to write: [$dst], error: $!\n", 2);
	print $write $lines;
	$write->close();
	record($conf, $log, __LINE__, " - Written.\n");
	
	return;
}

# 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, __LINE__.": Got args:\n") if $conf->{'system'}{debug};
	my $os=$conf->{'system'}{os_name};
	my $set_next="";
	foreach my $arg (@ARGV)
	{
		record($conf, $log, __LINE__, "[$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}{$os}{$set_next}=$arg;
			record($conf, $log, __LINE__.": Setting: 'install::$set_next': [$conf->{install}{$os}{$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}{$os}{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, __LINE__.": 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, __LINE__, "\nERROR: Argument: [$arg] is not valid!\n");
			record($conf, $log, __LINE__, "ERROR: Please run './fence_na --help' to see a list of valid arguments.\n\n");
			$bad=1;
		}
	}
}
