#!/usr/bin/perl
#
# Node Assassin - Uninstaller
# 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",
		install_log	=>	"./install.log",
		'log'		=>	"./uninstall.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/",
			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.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",
		},
	},
};

# Make sure I am running as root.
# print "Real UID: [$<], Effective UID: [$>]\n";
die "The Node Assassin uninstaller 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);

# 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 uninstall of Node Assassin:\n");
foreach my $key (sort {$b cmp $a} keys %{$conf->{install}{files}})
{
	next if $key eq "cluster_ng";
	my $dst=$conf->{install}{directory}{prefix}.$conf->{install}{directory}{$key}.$conf->{install}{files}{$key};
	remove_file($conf, $log, $dst);
}
unmerge_cluster_ng($conf, $log);

# Before I open the log file for writting, first read it in and look for a list
# of directories to try to remove.
my @remove_dirs;
record($conf, $log, "Will read the install log to see what directories I should remove.\n");
if (-f $conf->{'system'}{install_log})
{
	record($conf, $log, " - Found: [$conf->{'system'}{install_log}], reading it in.\n");
	my $read=IO::Handle->new();
	open ($read, "<$conf->{'system'}{install_log}") or record($conf, $log, "Failed to read: [$conf->{'system'}{install_log}], error: $!\n", 1);
	while (<$read>)
	{
		chomp;
		my $line=$_;
		if ($line=~/^ - Created: \[(.*?)\]/)
		{
			my $dir=$1;
			push @remove_dirs, $dir;
			record($conf, $log, " - Queueing : [$dir] for removal.\n");
		}
	}
}

# Remove the directories now.
foreach my $dir (sort {$b cmp $a} @remove_dirs)
{
	remove_directory($conf, $log, $dir);
}

record($conf, $log, "\nUninstallation completed!\n\n", 1);

exit(0);


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

sub remove_directory
{
	my ($conf, $log, $dir)=@_;
	
	print "Removing: [$dir]\n";
	rmdir($dir) or record($conf, $log, "Failed to remove the directory: [$dir], error: $!\n", 1);
	
	return;
}

sub remove_file
{
	my ($conf, $log, $file)=@_;
	
	record($conf, $log, "Checking if: [$file] exists; ");
	if (-f $file)
	{
		record($conf, $log, "it does, removing it... ");
		unlink($file) or record($conf, $log, "Failed to remove: [$file], error: $!\n", 1);
		record($conf, $log, "done.\n");
	}
	else
	{
		record($conf, $log, "it doesn't, nothing to remove.\n");
	}
	
	return;
}

sub unmerge_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 $file=$conf->{install}{directory}{prefix}.$conf->{install}{directory}{cluster_ng}.$conf->{install}{files}{cluster_ng};
	
	# Read in the source file.
	record($conf, $log, "Reading in: [$file] and parsing out Node Assassin support; ");
	my $read=IO::Handle->new();
	my $shell_call="<$file";
	open ($read, $shell_call) or record($conf, $log, "Failed to read: [$file], error: $!\n", 1);
	my $skip=0;
	while (<$read>)
	{
		my $line=$_;
		if (lc($line) =~ /node assassin/)
		{
			$skip=1;
			next;
		}
		if (($skip == 1) && ($line =~ /<!--/))
		{
			$skip=0;
		}
		next if $skip;
		$lines.=$line;
	}
	$read->close();
	record($conf, $log, "Read.\n");
	
	# Write it out.
	record($conf, $log, "Writing out: [$file]; ");
	my $write=IO::Handle->new();
	$shell_call=">$file";
	open ($write, $shell_call) or record($conf, $log, "Failed to write: [$file], error: $!\n", 1);
	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 record($conf, $log, "Failed to read: [$shell_call], error was: $!\n", 1);
	# 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);
}
