#!/usr/bin/perl # # Fence wrapper to simplify complex calls to PDU fence agents in pacemaker. # See README for details # # Alteeve's Niche! # Madison Kelly; mkelly@alteeve.ca # http://alteeve.ca/w # # This software is released under the GPL v2+. # # Bugs; # - None known, many expected # # Notes; # - Requires that the 'ethtool' package be installed. # # Play safe! use strict; use warnings; use IO::Handle; # Catch signals for clean exits. $SIG{INT} = \&_catch_sig; $SIG{TERM} = \&_catch_sig; # These are the default values and will be over-written by the config file's # variables which in turn can, in some cases, be over-written by command line # arguments. my $conf={ 'system' => { agent_version => "1.0", debug => 0, device => "", got_cla => 0, # This is set if command line arguments are read. 'log' => "/var/log/fence_multi.log", quiet => 0, version => 0, }, }; # Log file for output. my $log = IO::Handle->new(); print "Opening: [$conf->{'system'}{'log'}] for logging.\n" if $conf->{'system'}{debug}; open ($log, ">>$conf->{'system'}{'log'}") || die "Failed to open: [$conf->{'system'}{'log'}] for writing; Error: $!\n"; # Set $log and STDOUT 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); # Now read in arguments from STDIN, which is how 'fenced' passes arguments. ($bad) = read_stdin($conf, $log, $bad); # If I've been asked to show the metadata XML, do so and then exit. if ($conf->{'system'}{action} eq "metadata") { metadata($conf, $log); do_exit($conf, $log, 0); } # If I've been asked to show the version information, do so and then exit. if ($conf->{'system'}{version}) { version($conf, $log); do_exit($conf, $log, 0); } # Start the logs. record($conf, $log, "-=] Called at: [".get_date_time($conf)."]\n", 2); record($conf, $log, "system::fence-agent: [$conf->{'system'}{fence-agent}]\n", 2); record($conf, $log, "system::action: [$conf->{'system'}{action}]\n", 2); record($conf, $log, "system::addresses: [$conf->{'system'}{addresses}]\n", 2); record($conf, $log, "system::ports: [$conf->{'system'}{ports}]\n", 2); record($conf, $log, "system::pass_on_args: [$conf->{'system'}{pass_on_args}]\n", 2); # Assemble the calls by parsing the addresses and ports. foreach my $address (split /,/, $conf-> exit 0; ############################################################################### # Functions # ############################################################################### # This cleanly exits the agent. sub do_exit { my ($conf, $log, $exit_status) = @_; $exit_status = 9 if not defined $exit_status; # Close the log file handle, if it exists. print $log "\n"; $log->close() if $log; exit ($exit_status); } # This returns the current date and time. sub get_date_time { my ($conf) = @_; # Get the current date and time, my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time); # Format it to 'YYYY-MM-DD HH:MM:SS'. my $now=(1900+$year)."-".sprintf("%02d", ($mon+1))."-".sprintf("%02d", $mday)." ".sprintf("%02d", $hour).":".sprintf("%02d", $min).":".sprintf("%02d", $sec); return($now); } # This returns the 'help' message. sub help { my ($conf, $log) = @_; # Point the user at the man page. print "See 'man fence_multi' for instructions on using the Multi-PDU Fence Agent.\n"; print " - NOTE: ToDo; write man page. For now, see: \n"; print " https://github.com/digimer/fence_multi \n"; do_exit($conf, $log, 0); } # This simply prints the 'metadata' XML data to STDOUT. sub metadata { my ($conf, $log) = @_; print q` This agent is designed to allow a single fence primitive to be created in pacemaker that converts a "reboot" option into distinct "off" and "on" calls to two or more PDUs. This ensures that the PDU outlets feeding the target node are all off at the same time, ensuring the target loses power. As per the FenceAgentAPI, this wrapper will return success if both/all requested PDUs successfully complete their "off" action. An "on" action will be called after successful "off", but if the "on" action fails, the overall fence action is still considered successful. This agent supports "reboot", "off", "on", "status", "list" and "monitor" actions. Example useage; fence_multi -f fence_apc_snmp -a a:pdu1,b:pdu2 -n a:1,b:1 -o reboot This will call "fence_apc_snmp" twice with the "off" action; Once for "pdu1, port 1" and once for "pdu2, port 1". These ports will then be verified "off" and overall fence action will be determined. If successful, two more calls will be made to the same PDUs and ports with the "on" action. In the above example, "a" and "b" are used to link the PDU device with the desired port to act on. There is no restriction of the prefix used, they simply have to match between the PDU addresses and the ports. If either an address or a port does not have a corresponding partner, the agent will fail. Any attribute pair passed to this agent that is not recognized by this agent will be passed "as-is" to each call made to the specified fence agent. This is the actual fence agent to be called. This is a list of device IPs or hostnames to work on. Format is 'a:devX,b:devY' where {a,b} are prefixes matching entries in the 'port' list and {devX,devY} are the IPs or names of the devices to connect to. This is a list of ports to work on. Format is 'a:x,b:y' where {a,b} are prefixes matching entries in the 'ipaddr' list and {x,y} are the relative ports. Action (operation) to take; 'off', 'on', 'reboot', 'status', 'monitor', 'list' and 'metadata' are valid. The default "reboot" action is split into separate "off" and "on" calls, one of each per device:port pait. The "off" portion" is verified before "on" is called. Only "off" needs to succeed. The failure of any device or port causes the agent to fails. (default) Supress all output to STDOUT, including critical messages. Check logfile if used. Default 1 (quiet). Prints the fence agent version and exits. Wait X seconds before fencing is started Login Name Login password or passphrase Specifies SNMP version to use (1,2c,3) Set the community string TCP/UDP port to use for connection with device. fence_wti defaults to "23" or "22", if ssh is used. Forces agent to use IPv4 addresses only Forces agent to use IPv6 addresses only Script to retrieve password Set authentication protocol (MD5|SHA) Set security level (noAuthNoPriv|authNoPriv|authPriv) Set privacy protocol (DES|AES) Set privacy protocol password Script to run to retrieve privacy password Verbose mode Write debug information to given file Separator for CSV created by operation list Test X seconds for status change after ON/OFF Wait X seconds for cmd prompt after issuing command Wait X seconds for cmd prompt after login Wait X seconds after issuing ON/OFF Count of attempts to retry power on SSH connection Identity file for ssh SSH options to use Name of hidden page Force command prompt `; # Done, exit. 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}; $conf->{'system'}{got_cla}=1; # If 'set_next' has a value, push this argument into the 'conf' # hash. if ($set_next) { # Record the values. $conf->{'system'}{$set_next} = $arg; record($conf, $log, "Setting: 'system::$set_next': [$conf->{'system'}{$set_next}]\n") if $conf->{'system'}{debug}; # Clear it now for the next go-round. $set_next=""; next; } elsif ($arg =~ /^--(.*?)=(.*)/) { # Parse out all my $variable = $1; my $value = $2; $conf->{'system'}{$variable} = $value; record($conf, $log, "Setting: 'system::$variable': [$conf->{'system'}{$variable}]\n") if $conf->{'system'}{debug}; # If this isn't one of my switches, record it for pass-through. if ($variable ne "fence-agent") { $conf->{'system'}{pass_on_args} .= "$arg "; record($conf, $log, "Appending: 'system::pass_on_args': [$conf->{'system'}{pass_on_args}\n") if $conf->{'system'}{debug}; } next; } # Pick out the args I care about. if ($arg =~ /-h/) { # Print the help message and then exit. help($conf, $log); } elsif (($arg =~ /-V/) or ($arg =~ /--version/)) { # Print the version information and then exit. $conf->{'system'}{version} = 1; record($conf, $log, "Setting version\n") if $conf->{'system'}{debug}; } elsif (($arg =~ /-f/) or ($arg =~ /--fence-agent/)) { # Print the version information and then exit. $set_next = "fence-agent"; next; } elsif ($arg =~ /-a/) { # Record the ip names/addresses. $set_next = "ipaddr"; next; } elsif ($arg =~ /-n/) { # Record the outlets. $set_next = "port"; next; } elsif ($arg =~ /-o/) { # Record the requested action. $set_next = "action"; next; } elsif ($arg =~ /-q/) { # Suppress all messages, including critical messages, from STDOUT. $conf->{'system'}{quiet} = 1; } elsif ($arg =~ /-d/) { # Enable debug mode. $conf->{'system'}{debug} = 1; } else { # Not one of my switches, append it to the pass-through $conf->{'system'}{pass_on_args} .= "$arg "; record($conf, $log, "Appending: 'system::pass_on_args': [$conf->{'system'}{pass_on_args}]\n") if $conf->{'system'}{debug}; } } } # Read arguments from STDIN. This is adapted from the 'fence_brocade' agent. sub read_stdin { my ($conf, $log, $bad) = @_; return (0) if $conf->{'system'}{got_cla}; 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", 2); # 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. my ($variable, $value) = split /\s*=\s*/, $option; # Record the line for now, but comment this out before release. #record ($conf, $log, "Variable: [$variable], value: [$value].\n") if $conf->{'system'}{debug}; record ($conf, $log, "Variable: [$variable], value: [$value].\n"); # Set my variables depending on the veriable name. if ($variable eq "fence-agent") { # This is the fence-agent I will call $conf->{'system'}{fence-agent} = $value; } elsif ($variable eq "action") { # The desired action. $conf->{'system'}{action} = $value; } elsif ($variable eq "ipaddr") { # The names/IP addresses of the nodes. $conf->{'system'}{addresses} = $value; } elsif ($variable eq "port") { # This sets the port number to act on. $conf->{'system'}{ports} = $value; } else { $conf->{'system'}{$variable} = $value; record($conf, $log, "Setting: 'system::$variable': [$conf->{'system'}{$variable}]\n") if $conf->{'system'}{debug}; next; } } return ($bad); } # 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. if ($critical == 2) { print $log $msg; } print $msg if ((not $conf->{'system'}{quiet}) && ($critical != 2)); # Critical messages always print. print $msg if (($critical) && ($conf->{'system'}{quiet})); return(0); } # 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. record ($conf, $log, "Fence Agent ver. $conf->{'system'}{agent_version}\n", 1); do_exit($conf, $log, 0); } # Catch SIG, move zig! sub _catch_sig { my $signame = shift; record($conf, $log, "fence_passive_nic process with PID $$ Exiting on SIG${signame}.\n", 1); do_exit($conf, $log, 1); }