Fence na.lib

From Alteeve Wiki
Revision as of 06:43, 5 March 2010 by Digimer (talk | contribs) (Created page with '{{na_header}} '''NOTE''': The comments in this file need to be update, please don't trust them. This is the fence agent's function library that exists in <span class="code">/et…')
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

 Node Assassin :: Fence na.lib

NOTE: The comments in this file need to be update, please don't trust them.

This is the fence agent's function library that exists in /etc/na/.

#!/usr/bin/perl
#
# This is the function library for the Node Assassin fence agent.
# 
# Node Assassin - Fence Agent
# Digimer; digimer@alteeve.com
# Mar. 05, 2010.
# Version: 0.1.003
#


# This cleanly exits the agent.
sub do_exit
{
	($conf, $log, $exit_status)=@_;
	$exit_status=9 if not defined $exit_status;
	
	$conf->{node}{handle}->close;
	$log->close();
	exit ($exit_status);
}

# This returns the 'help' message.
sub help
{
	my ($conf, $log)=@_;
	my $msg=q`
NOTE: This is now out of date!
	
Node Assassin Fencing Agent

	This program interfaces with the Node Assassin to set one or more nodes
	to one or more states.

Usage:

	./fence_na <options>

Overview:

	This takes one or more arguments relating to the desired state to set a
	node to follow by one or more Node IDs to act on. Multiple states can
	be set at the same time.

	When specifying a single node, pass a single ID (not zero-padded).

	When specifying two or more nodes, seperate them with a comma with no
	spaces.

States:

	0
		This state will fence the nodes specified by the list.
	
	1
		This will release the fence and allow the specified node(s) to
		boot.
	
	2
		This will fence the node(s) for one second. This is meant to be
		used on ports connected to a node's power button. If the node
		is alive and supports ACPI, this should start a graceful power
		down of the node. Conversly, if the node was off, this will
		boot the node. When connected to a node's reset switch, this
		will cause a quick reboot without a graceful power off.
	
	3
		This state will fence the node(s) for five seconds. This is
		specifically designed for ports connected to a node's power
		button. It will allow a frozen node to be forced off by holding
		the power button closed long enough to force a power off. This
		state serves no real difference over state 2 when connected to
		a reset switch.

Options:

	In all cases, '--set_state_X=<list>' and '-X <list>' are equal and
	interchangable. The examples below will use the long-form arguments for
	the sake of clarity.
	
	--set_state_0=<list>
	
		This sets the specified port(s) state 0.
	
	--set_state_1=<list>
	
		This sets the specified port(s) state 0.

	--set_state_2=<list>
	
		This sets the specified port(s) state 2.
	
	--set_state_2=<list>
	
		This sets the specified port(s) state 2.

Examples:

	Fence node 1.
	
		./fence_na --set_state_0=1
	
	Release the fence on node 1.
	
		./fence_na --set_state_1=1
	
	Boot nodes 1 and 2
	
		./fence_na --set_state_2=1,2
	
	Force node 2 to power off.
	
		./fence_na --set_state_3=2
	
	Fence nodes 4 and 5 then boot node 6
	
		./fence_na --set_state_0=4,5 --set_state_2=6
		
Note:

	An internal pager is not implemented. You may wish to run this via
	'less':
	
	./fence_na | less

NOTE: This is now out of date!
`;
	print $msg;
	
	do_exit($conf, $log, 0);
}

# This handles the actual actions.
sub process_action
{
	my ($conf, $log)=@_;
	
	# Make this more readable.
	my $na_id=$conf->{'system'}{node_assassin_id};
	my $action=$conf->{node}{action};
	my $port=$conf->{node}{port};
	
	# Translate the port passed in by the fence agent into the actual ports
	# in the Node Assassin. Mapping is:
	# Node 01 -> Power = Port 01
	# Node 01 -> Reset = Port 02
	# Node 02 -> Power = Port 03
	# Node 02 -> Reset = Port 04
	# Node 03 -> Power = Port 05
	# Node 03 -> Reset = Port 06
	# Node 04 -> Power = Port 07
	# Node 04 -> Reset = Port 08
	# ...
	my $power_port=sprintf("%02d", (($port*2)-1));
	my $reset_port=sprintf("%02d", ($port*2));
	record($conf, $log, "Translated node port: [$port] to power port: [$power_port] and reset port: [$reset_port]\n");
	
	if ($action eq "on")
	{
		# Release the fence and boot the node.
		$conf->{'system'}{call_order}="$reset_port:1,$power_port:1,sleep,$power_port:2";
	}
	elsif ($action eq "off")
	{
		# Fence the node by pressing and holding the reset to make sure
		# the node immediately dies. Then I release the fence long
		# enough to force a power off, then I re-apply then fence to
		# make sure the node doesn't come back up. This is needed
		# because some machines won't power off if the reset is held
		# high when the power is pressed, even for > 4 seconds.
		$conf->{'system'}{call_order}="$reset_port:0,sleep,$reset_port:1,sleep,$power_port:0,sleep 5,$reset_port:0";
	}
	elsif ($action eq "reboot")
	{
		# Currently, I don't do this gracefully because, well, if it's
		# being fenced, it's not meant to be graceful.
		# This is a combination of the 'off' -> 'on' actions.
		$conf->{'system'}{call_order}="$reset_port:0,sleep,$reset_port:1,sleep,$power_port:0,sleep 5,$reset_port:0";
		$conf->{'system'}{call_order}.=",$reset_port:1,$power_port:1,sleep,$power_port:2";
	}
	elsif ($action eq "status")
	{
		# This should check the probe, but for now, it checks the
		# port's state.
	}
	elsif (($action eq "monitor") or ($action eq "list"))
	{
		# Not sure what to do here.
	}
	else
	{
		record($conf, $log, "Unknown action request: [$action]!\n");
		do_exit($conf, $log, 9);
	}
}

# 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}";
# 	print "Shell call: [$shell_call]\n";
	open ($read, "<$shell_call") or die "Failed to read: [$shell_call], error was: $!\n";
	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);
# 		print "Storing: [$var] = [$val]\n";
		_make_hash_reference($conf, $var, $val);
	}
	$read->close();
	
	return (0);
}

# Read in command line arguments
sub read_cla
{
	my ($conf, $log, $bad)=@_;
	
	# MADI: Remove this before release.
	record($conf, $log, "Got args:\n");
	
	# Loop through the passed arguments, if any.
	my $set_next="";
	foreach my $arg (@ARGV)
	{
		# MADI: Remove this before release.
		record($conf, $log, "[$arg]\n");
		
		# 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->{node}{$set_next}=$arg;
			
			# MADI: Remove this before release.
			record($conf, $log, "Setting: 'node::$set_next': [$conf->{node}{$set_next}]\n");
			
			# 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=~/-[vV]/)
		{
			# Print the version information and then exit.
			$conf->{'system'}{version}=1;
		}
		elsif ($arg=~/-q/)
		{
			# Suppress all non-critical messages from STDOUT.
			$conf->{'system'}{quiet}=1;
		}
		elsif ($arg=~/^-/)
		{
			$arg=~s/^-//;
			
			### These are the switches set by Red Hat.
			if ($set_next eq "a")
			{
				# This is the IP address or hostname of the
				# Node Assassin to call.
				$set_next="ipaddr";
			}
			elsif ($set_next eq "l")
			{
				# This is the login name.
				$set_next="login";
			}
			elsif ($set_next eq "p")
			{
				# This is the password. If it starts with '/'
				# it is interpreted to be a file containing the
				# password which will be read in and it's
				# contents will replace# this value.
				$set_next="passwd";
			}
			elsif ($set_next eq "n")
			{
				# This is the node to work on.
				$set_next="port";
			}
			elsif ($set_next eq "o")
			{
				# This is the action to take. Valid actions
				# are:
				# on      = ##:1		# Release fence
				# off     = ##:0		# Fence
				# reboot  = ##:3 -> ##:2	# Force off then boot.
				# status  = Returns the node's current status.
				# monitor = Returns the status of all nodes.
				$set_next="action";
			}
			elsif ($set_next eq "S")
			{
				# This is the script to run to retrieve the
				# password when it is not stored in
				# 'cluster.conf'. This script should echo/print
				# the password to STDOUT.
				$set_next="passwd_script";
			}
		}
		else
		{
			### MADI: I might want to pick up arguments via multiple lines.
			# Bad argument.
			record($conf, $log, "Argument: [$arg] is not valid!\n");
			record($conf, $log, "Please run './fence_na --help' to see a list of valid arguments.\n");
			$bad=1;
		}
	}
}

# Read arguments from STDIN. This is adapted from the 'fence_brocade' agent.
sub read_stdin
{
	my ($conf, $log, $bad)=@_;
	
	my $option;
	my $line_count=0;
	while(defined (my $option=<>))
	{
		# Get rid of newlines.
		chomp $option;
		
		# Record the line for now, but comment this out before release.
		record ($conf, $log, "Processing option line: [$option]\n");
		
		# strip leading and trailing whitespace
		$option=~s/^\s*//;
		$option=~s/\s*$//;
		
		# skip comments
		next if ($option=~ /^#/);
		
		# Increment my option line count.
		$line_count++;
		
		# Go to the next line if the option line is empty.
		next if not $option;
		
		# Split the option up into the name and the value.
		($name,$value)=split /\s*=\s*/, $option;
		
		# Record the line for now, but comment this out before release.
		record ($conf, $log, "Name: [$name], value: [$value].\n");
		
		# Set my variables depending on the veriable name.
		if ($name eq "agent")
		{
			# This is only used by 'fenced', but I record it for
			# potential debugging.
			$conf->{node}{agent}=$value;
		}
		elsif ($name eq "fm")
		{
			# This is a deprecated argument that should no longer
			# be used. Now 'port' should be used.
			if (not $conf->{node}{port})
			{
				# Port isn't set yet, use this value which may
				# be replaced if 'port' is set later.
				(undef, $value) = split /\s+/,$value;
				$conf->{node}{port}=$value;
				warn "Warning! The argument 'fm' is deprecated, use 'port' instead. Value: [$value] set for 'port'\n";
			}
			else
			{
				# Port was already set, so simply ignore this.
				warn "Warning! The argument 'fm' is deprecated, use 'port' instead. Value: [$value] ignored.\n";
			}
		}
		elsif ($name eq "ipaddr") 
		{
			# Record the IP Address or name of the Node Assassin to
			# use.
			$conf->{node}{ipaddr}=$value;
		} 
		elsif ($name eq "login")
		{
			# Record the login name that was passed.
			$conf->{node}{login}=$value;
		} 
		elsif ($name eq "name")
		{
			# Depricated argument used formerly for login name.
			if (not $conf->{node}{login})
			{
				# Login isn't set yet, use this value which may
				# be replaced if 'login' is seen later.
				$conf->{node}{login}=$value;
				warn "Warning! The argument 'name' is deprecated, use 'login' instead. Value: [$value] set for 'login'.\n";
			}
			else
			{
				# I've already seen the 'login' value so I will
				# ignore this value.
				warn "Warning! The argument 'name' is deprecated, use 'login' instead. Value: [$value] ignored.\n";
			}
		}
		elsif (($name eq "action") or ($name eq "option"))
		{
			# It looks like 'option' is going to be deprecated in
			# favour of 'action'. If/when that happens, add a warn.
			$conf->{node}{action}=$value;
		}
		elsif ($name eq "passwd")
		{
			# This is the login password.
			$conf->{node}{passwd}=$value;
		} 
		elsif ($name eq "passwd_script")
		{
			# This is the path to the script that will return the
			# password to the agent. At this time, this is not
			# implemented.
			$conf->{node}{passwd_script}=$value;
		}
		elsif ($name eq "port")
		{
			# This sets the port number to act on.
			$conf->{node}{port}=$value;
		} 
		else
		{
			warn "Illegal name in option: [$option] at line: [$line_count]\n";
			$bad=1;
		}
	}
	return ($bad);
}

# This function simply prints messages to both the log and to stdout.
sub record
{
	my ($conf, $log, $msg)=@_;
	
	print $log $msg;
	print $msg if not $conf->{'system'}{quiet};
	
	return(0);
}

# When asked to 'monitor' or 'list', do this... whatever 'this' is. All I know
# is that it should not generate output.
sub show_list
{
	my ($conf, $log)=@_;
	
	### MADI: No idea what will be needed here, so here are both queries.
	###       Make them available elsewhere if not used here.
	record($conf, $log, "Checking states:\n");
	my @state_out=$conf->{node}{handle}->cmd("00:0");
	foreach my $line (@state_out)
	{
# 		record($conf, $log, $line);
	}
	record($conf, $log, "Done.\n");

	# Query states and Node Assassin info.
	record($conf, $log, "Checking Node Assassin info:\n");
	my @info_out=$conf->{node}{handle}->cmd("00:1");
	my $node_name="";
	foreach my $line (@info_out)
	{
		record($conf, $log, $line);
		$node_name=$1 if $line=~/- Node Name: ..... (.*)/;
	}
	record($conf, $log, "Node name: [$node_name]\n");
	record($conf, $log, "Done.\n");
	
	do_exit($conf, $log, 0);
}

# This queries the Node Assassin and returns the state of the requested node.
sub show_state
{
	my ($conf, $log)=@_;
	
	my @state_out=$conf->{node}{handle}->cmd("00:0");
	my $state="";
	my $node=$conf->{node}{port};
	foreach my $line (@state_out)
	{
		chomp;
		my $line=$_;
		my ($state)=($line=~/- Node $node: (.*?)/);
		if ($state)
		{
			$state=lc($state)=~/fenced/ ? 2 : 0;
			last;
		}
	}
	# No state means something went wrong while talking to the Node
	# Assassin.
	$state=1 if (($state != 0) && ($state != 2));
	
	# As per: http://sources.redhat.com/cluster/wiki/FenceAgentAPI
	# The exit state must be:
	# 0 = Node is running
	# 1 = Failed to contact fence, unknown state.
	# 2 = Node is fenced.
	do_exit($conf, $log, $state);
}

# This prints the version information of this fence agent and of any configured
# fence devices.
sub version
{
	my ($conf, $log)=@_;
	
	# Print the Fence Agent version first.
	print "Fence Agent: ......... Node Assassin ver. $conf->{'system'}{agent_version}\n";
	print "Configured Nodes: .... $conf->{'system'}{nodes}\n";
	for my $node (1..$conf->{'system'}{nodes})
	{
		print " - Node $node Name: .. $conf->{node}{$node}{name}\n";
		print " - Node $node IP: .... $conf->{node}{$node}{ip}\n";
		print " - Node $node Port: .. $conf->{node}{$node}{port}\n";
		print " - Node $node MAC: ... $conf->{node}{$node}{mac}\n";
		print " - Node $node Netmask: $conf->{node}{$node}{ip}\n";
		print " - Node $node Gateway: $conf->{node}{$node}{ip}\n";
	}
	do_exit($conf, $log, 0);
}


###############################################################################
# Private functions below here.                                               #
###############################################################################

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

1;

 

Input, advice, complaints and meanderings all welcome!
Digimer digimer@alteeve.ca https://alteeve.ca/w legal stuff:  
All info is provided "As-Is". Do not use anything here unless you are willing and able to take resposibility for your own actions. © 1997-2013
Naming credits go to Christopher Olah!
In memory of Kettle, Tonia, Josh, Leah and Harvey. In special memory of Hannah, Jack and Riley.