diff options
Diffstat (limited to 'lib/plugins/stonith/external')
20 files changed, 4726 insertions, 0 deletions
diff --git a/lib/plugins/stonith/external/Makefile.am b/lib/plugins/stonith/external/Makefile.am new file mode 100644 index 0000000..42e0046 --- /dev/null +++ b/lib/plugins/stonith/external/Makefile.am @@ -0,0 +1,33 @@ +# Makefile.am for OCF RAs +# +# Author: Sun Jing Dong +# Copyright (C) 2004 IBM +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +MAINTAINERCLEANFILES = Makefile.in + +EXTRA_DIST = drac5 dracmc-telnet ibmrsa-telnet ipmi rackpdu vmware vcenter xen0 \ + xen0-ha-dom0-stonith-helper kdumpcheck nut + +extdir = $(stonith_ext_plugindir) + +helperdir = $(stonith_plugindir) + +ext_SCRIPTS = drac5 dracmc-telnet ibmrsa ibmrsa-telnet ipmi riloe ssh vmware vcenter rackpdu xen0 hmchttp \ + xen0-ha kdumpcheck ippower9258 nut libvirt \ + hetzner + +helper_SCRIPTS = xen0-ha-dom0-stonith-helper diff --git a/lib/plugins/stonith/external/drac5.in b/lib/plugins/stonith/external/drac5.in new file mode 100644 index 0000000..218cbd3 --- /dev/null +++ b/lib/plugins/stonith/external/drac5.in @@ -0,0 +1,113 @@ +#!/bin/sh +# +# External STONITH module for DRAC5 adapters. +# +# Author: Jun Wang +# License: GNU General Public License (GPL) +# + +trap 'if [ -n "$outf" ]; then ha_log.sh err "`cat $outf`"; rm -f "$outf"; fi' 0 +outf=`mktemp` || { + ha_log.sh err "mktemp failed" + exit 1 +} + +sshlogin() { + if [ x = "x$ipaddr" -o x = "x$userid" ] + then + ha_log.sh err "ipaddr or userid missing; check configuration" + return 1 + fi + @SSH@ -q -x -n $userid@$ipaddr racadm serveraction "$1" >$outf 2>&1 +} + +drac_reset() { + sshlogin hardreset +} + +drac_on() { + sshlogin poweron +} + +drac_off() { + sshlogin poweroff +} + +drac_status() { + sshlogin powerstatus +} + +case $1 in +gethosts) + echo $hostname + ;; +on) + drac_poweron + ;; +off) + drac_poweroff + ;; +reset) + drac_reset + ;; +status) + drac_status + ;; +getconfignames) + for i in hostname ipaddr userid; do + echo $i + done + ;; +getinfo-devid) + echo "DRAC5 STONITH device" + ;; +getinfo-devname) + echo "DRAC5 STONITH device" + ;; +getinfo-devdescr) + echo "DRAC5 host reset/poweron/poweroff" + ;; +getinfo-devurl) + echo "http://www.dell.com" + ;; +getinfo-xml) + cat <<EOF +<parameters> + +<parameter name="hostname" unique="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostname +</shortdesc> +<longdesc lang="en"> +The hostname of the host to be managed by this STONITH device +</longdesc> +</parameter> + +<parameter name="ipaddr" unique="1"> +<content type="string" /> +<shortdesc lang="en"> +IP Address +</shortdesc> +<longdesc lang="en"> +The IP address of the STONITH device +</longdesc> +</parameter> + +<parameter name="userid" unique="1"> +<content type="string" /> +<shortdesc lang="en"> +Login +</shortdesc> +<longdesc lang="en"> +The username used for logging in to the STONITH device +</longdesc> +</parameter> + +</parameters> +EOF + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/dracmc-telnet b/lib/plugins/stonith/external/dracmc-telnet new file mode 100644 index 0000000..d993961 --- /dev/null +++ b/lib/plugins/stonith/external/dracmc-telnet @@ -0,0 +1,377 @@ +#!/usr/bin/env python +# vim: set filetype=python +####################################################################### +# +# dracmc-telnet - External stonith plugin for HAv2 (http://linux-ha.org/wiki) +# Connects to Dell Drac/MC Blade Enclosure via a Cyclades +# terminal server with telnet and switches power of named +# blade servers appropriatelly. +# +# Required parameters: +# nodename: The name of the server you want to touch on your network +# cyclades_ip: The IP address of the cyclades terminal server +# cyclades_port: The port for telnet to access on the cyclades (i.e. 7032) +# servername: The DRAC/MC server name of the blade (i.e. Server-7) +# username: The login user name for the DRAC/MC +# password: The login password for the DRAC/MC +# +# Author: Alex Tsariounov <alext@novell.com> +# +# Based on ibmrsa-telnet external stonith plugin by Andreas Mock +# (andreas.mock@web.de), Copyright by Adreas Mock and released as part +# of HAv2. +# +# History: +# 2009-10-12 First release. +# +# Copyright (c) 2009 Novell, Inc. +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 or later of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# +####################################################################### +import sys +import os +import time +import telnetlib +import random +import subprocess + +LOGINRETRIES = 10 + +class TimeoutException(Exception): + def __init__(self, value=None): + Exception.__init__(self) + self.value = value + + def __str__(self): + return repr(self.value) + +class DracMC(telnetlib.Telnet): + def __init__(self, *args, **kwargs): + telnetlib.Telnet.__init__(self) + self._timeout = 4 + self._loggedin = 0 + self._history = [] + self._appl = os.path.basename(sys.argv[0]) + self._server = args[0] + + def _get_timestamp(self): + ct = time.time() + msecs = (ct - long(ct)) * 1000 + return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(ct)), msecs) + + def write(self, buffer): + self._history.append(self._get_timestamp() + ': WRITE: ' + repr(buffer)) + telnetlib.Telnet.write(self, buffer) + + def read_until(self, what, timeout=2): + line = telnetlib.Telnet.read_until(self, what, timeout) + self._history.append(self._get_timestamp() + ': READ : ' + repr(line)) + if not line.endswith(what): + raise TimeoutException("Timeout while waiting for '%s'." % (what, )) + return line + + def login(self, user, passwd): + time.sleep(0.3) + try: + line = self.read_until('Login: ', self._timeout) + self.write(user) + self.write('\r') + line = self.read_until('Password: ', self._timeout) + self.write(passwd) + self.write('\r') + except: + self.write("\r") + line = self.read_until('Login: ', self._timeout) + self.write(user) + self.write('\r') + line = self.read_until('Password: ', self._timeout) + self.write(passwd) + self.write('\r') + try: + line = self.read_until('DRAC/MC:', self._timeout) + except: + self.write("\r") + line = self.read_until('DRAC/MC:', self._timeout) + + def hardreset(self): + self.write('serveraction -s %s hardreset\r' % self._server) + line = self.read_until('OK', 10) + line = self.read_until('DRAC/MC:', self._timeout) + + def powercycle(self): + self.write('serveraction -s %s powercycle\r' % self._server) + line = self.read_until('OK', 10) + line = self.read_until('DRAC/MC:', self._timeout) + + def on(self): + self.write('serveraction -s %s powerup\r' % self._server) + line = self.read_until('OK', 10) + line = self.read_until('DRAC/MC:', self._timeout) + + def off(self): + self.write('serveraction -s %s powerdown\r' % self._server) + line = self.read_until('OK', 10) + line = self.read_until('DRAC/MC:', self._timeout) + + def exit(self): + self.write('exit\r') + + def get_history(self): + return "\n".join(self._history) + + +class DracMCStonithPlugin: + def __init__(self): + # define the external stonith plugin api + self._required_cmds = \ + 'reset gethosts status getconfignames getinfo-devid ' \ + 'getinfo-devname getinfo-devdescr getinfo-devurl ' \ + 'getinfo-xml' + self._optional_cmds = 'on off' + self._required_cmds_list = self._required_cmds.split() + self._optional_cmds_list = self._optional_cmds.split() + + # who am i + self._appl = os.path.basename(sys.argv[0]) + + # telnet connection object + self._connection = None + + # the list of configuration names + self._confignames = ['nodename', 'cyclades_ip', 'cyclades_port', + 'servername', 'username', 'password'] + + # catch the parameters provided by environment + self._parameters = {} + for name in self._confignames: + try: + self._parameters[name] = os.environ.get(name, '').split()[0] + except IndexError: + self._parameters[name] = '' + + def _get_timestamp(self): + ct = time.time() + msecs = (ct - long(ct)) * 1000 + return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(ct)), msecs) + + def _echo_debug(self, *args): + subprocess.call("ha_log.sh debug '%s'" % ' '.join(args), shell=True) + + def echo(self, *args): + what = ''.join([str(x) for x in args]) + sys.stdout.write(what) + sys.stdout.write('\n') + sys.stdout.flush() + self._echo_debug("STDOUT:", what) + + def echo_log(self, level, *args): + subprocess.call("ha_log.sh %s '%s'" % (level,' '.join(args)), shell=True) + + def _get_connection(self): + if not self._connection: + c = DracMC(self._parameters['servername']) + self._echo_debug("Connecting to '%s:%s'" % + (self._parameters['cyclades_ip'], + self._parameters['cyclades_port'])) + tries = 0 + while tries < LOGINRETRIES: + try: + c.open(self._parameters['cyclades_ip'], + self._parameters['cyclades_port']) + c.login(self._parameters['username'], + self._parameters['password']) + except Exception, args: + if "Connection reset by peer" in str(args): + self._echo_debug("Someone is already logged in... retry=%s" % tries) + c.close() + time.sleep(random.uniform(1.0, 5.0)) + else: + raise + else: + break + tries += 1 + + if tries == LOGINRETRIES: + c.close() + raise Exception("Could not log in to %s:%s" % + (self._parameters['cyclades_ip'], + self._parameters['cyclades_port'])) + self._connection = c + + def _end_connection(self): + if self._connection: + self._connection.exit() + self._connection.close() + + def reset(self): + self._get_connection() + # self._connection.hardreset() + self._connection.powercycle() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Reset of node '%s' done" % + (self._parameters['nodename'],)) + return(0) + + def on(self): + self._get_connection() + self._connection.on() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Switched node '%s' ON" % + (self._parameters['nodename'],)) + return(0) + + def off(self): + self._get_connection() + self._connection.off() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Switched node '%s' OFF" % + (self._parameters['nodename'],)) + return(0) + + def gethosts(self): + self.echo(self._parameters['nodename']) + return(0) + + def status(self): + self._get_connection() + self._end_connection() + self._echo_debug(self._connection.get_history()) + return(0) + + def getconfignames(self): + for name in ['nodename', 'cyclades_ip', 'cyclades_port', 'servername', + 'username', 'password']: + self.echo(name) + return(0) + + def getinfo_devid(self): + self.echo("External Stonith Plugin for Dell DRAC/MC via Cyclades") + return(0) + + def getinfo_devname(self): + self.echo("External Stonith Plugin for Dell Drac/MC connecting " + "via Telnet to a Cyclades port") + return(0) + + def getinfo_devdescr(self): + self.echo("External stonith plugin for HAv2 which connects to " + "a Dell DRAC/MC connected via a Cyclades port with telnet. " + "Commands to turn on/off power and to reset server are sent " + "appropriately. " + "(c) 2009 by Novell, Inc. (alext@novell.com)") + return(0) + + def getinfo_devurl(self): + self.echo("http://support.dell.com/support/edocs/software/smdrac3/dracmc/1.3/en/index.htm") + + def getinfo_xml(self): + info = """<parameters> + <parameter name="nodename" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">nodename to shoot</shortdesc> + <longdesc lang="en"> + Name of the node to be stonithed. + </longdesc> + </parameter> + <parameter name="cyclades_ip" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">hostname or ip address of cyclades</shortdesc> + <longdesc lang="en"> + Hostname or IP address of Cyclades connected to DRAC/MC. + </longdesc> + </parameter> + <parameter name="cyclades_port" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">telnet port to use on cyclades</shortdesc> + <longdesc lang="en"> + Port used with the Cyclades telnet interface which is connected to the DRAC/MC. + </longdesc> + </parameter> + <parameter name="servername" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">DRAC/MC name of blade to be stonithed</shortdesc> + <longdesc lang="en"> + Name of server blade to be stonithed on the DRAC/MC (example: Server-7) + </longdesc> + </parameter> + <parameter name="username" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">username to login on the DRAC/MC</shortdesc> + <longdesc lang="en"> + Username to login to the DRAC/MC once connected via the Cyclades port. + </longdesc> + </parameter> + <parameter name="password" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">password to login on the DRAC/MC</shortdesc> + <longdesc lang="en"> + Password to login to the DRAC/MC once connected via the Cyclades port. + </longdesc> + </parameter> + </parameters> + """ + self.echo(info) + return(0) + + def not_implemented(self, cmd): + self.echo_log("err", "Command '%s' not implemented." % (cmd,)) + return(1) + + def usage(self): + usage = "Call me with one of the allowed commands: %s, %s" % ( + ', '.join(self._required_cmds_list), + ', '.join(self._optional_cmds_list)) + return usage + + def process(self, argv): + self._echo_debug("========== Start =============") + if len(argv) < 1: + self.echo_log("err", 'At least one commandline argument required.') + return(1) + cmd = argv[0] + self._echo_debug("cmd:", cmd) + if cmd not in self._required_cmds_list and \ + cmd not in self._optional_cmds_list: + self.echo_log("err", "Command '%s' not supported." % (cmd,)) + return(1) + try: + cmd = cmd.lower().replace('-', '_') + func = getattr(self, cmd, self.not_implemented) + rc = func() + return(rc) + except Exception, args: + self.echo_log("err", 'Exception raised:', str(args)) + if self._connection: + self.echo_log("err", self._connection.get_history()) + self._connection.close() + return(1) + + +if __name__ == '__main__': + stonith = DracMCStonithPlugin() + rc = stonith.process(sys.argv[1:]) + sys.exit(rc) diff --git a/lib/plugins/stonith/external/hetzner b/lib/plugins/stonith/external/hetzner new file mode 100755 index 0000000..2b3e675 --- /dev/null +++ b/lib/plugins/stonith/external/hetzner @@ -0,0 +1,139 @@ +#!/bin/sh +# +# External STONITH module for Hetzner. +# +# Copyright (c) 2011 MMUL S.a.S. - Raoul Scarazzini <rasca@mmul.it> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +# Read parameters from config file, format is based upon the hetzner OCF resource agent +# developed by Kumina: http://blog.kumina.nl/2011/02/hetzner-failover-ip-ocf-script/ +conf_file="/etc/hetzner.cfg" + +case $1 in + get*) ;; # don't print errors if conf_file not present + *) + user=`sed -n 's/^user.*=\ *//p' $conf_file` + pass=`sed -n 's/^pass.*=\ *//p' $conf_file` + ;; +esac + +hetzner_server="https://robot-ws.your-server.de" + +check_http_response() { + # If the response is 200 then return 0 + if [ $1 = 200 ] + then + return 0 + else + # If the response is not 200 then display a description of the problem and return 1 + case $1 in + 400) ha_log.sh err "INVALID_INPUT - Invalid input parameters" + ;; + 404) ha_log.sh err "SERVER_NOT_FOUND - Server with ip $remote_ip not found" + ;; + 409) ha_log.sh err "RESET_MANUAL_ACTIVE - There is already a running manual reset" + ;; + 500) ha_log.sh err "RESET_FAILED - Resetting failed due to an internal error" + ;; + esac + return 1 + fi +} + +case $1 in +gethosts) + echo $hostname + exit 0 + ;; +on) + # Can't really be implemented because Hetzner's webservice cannot power on a system + ha_log.sh err "Power on is not available since Hetzner's webservice can't do this operation." + exit 1 + ;; +off) + # Can't really be implemented because Hetzner's webservice cannot power on a system + ha_log.sh err "Power off is not available since Hetzner's webservice can't do this operation." + exit 1 + ;; +reset) + # Launching the reset action via webservice + check_http_response $(curl --silent -o /dev/null -w '%{http_code}' -u $user:$pass $hetzner_server/reset/$remote_ip -d type=hw) + exit $? + ;; +status) + # Check if we can contact the webservice + check_http_response "$(curl --silent -o /dev/null -w '%{http_code}' -u $user:$pass $hetzner_server/server/$remote_ip)" + exit $? + ;; +getconfignames) + echo "hostname" + echo "remote_ip" + exit 0 + ;; +getinfo-devid) + echo "Hetzner STONITH device" + exit 0 + ;; +getinfo-devname) + echo "Hetzner STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "Hetzner host reset" + echo "Manages the remote webservice for reset a remote server." + exit 0 + ;; +getinfo-devurl) + echo "http://wiki.hetzner.de/index.php/Robot_Webservice_en" + exit 0 + ;; +getinfo-xml) + cat << HETZNERXML +<parameters> +<parameter name="hostname" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostname +</shortdesc> +<longdesc lang="en"> +The name of the host to be managed by this STONITH device. +</longdesc> +</parameter> + +<parameter name="remote_ip" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Remote IP +</shortdesc> +<longdesc lang="en"> +The address of the remote IP that manages this server. +</longdesc> +</parameter> +</parameters> +HETZNERXML + exit 0 + ;; +*) + ha_log.sh err "Don't know what to do for '$remote_ip'" + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/hmchttp b/lib/plugins/stonith/external/hmchttp new file mode 100644 index 0000000..9d111bc --- /dev/null +++ b/lib/plugins/stonith/external/hmchttp @@ -0,0 +1,218 @@ +#!/bin/sh +# External STONITH module for HMC web console +# +# Copyright (c) 2007 Xinwei Hu <hxinwei@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +#set -x +hostlist=`echo $hostlist | tr ',' ' '` + +trap '[ ! -e "$COOKIEFILE" ] || rm -f "$COOKIEFILE"' 0 +COOKIEFILE=`mktemp` || exit 1 + +: ${CURLBIN="/usr/bin/curl"} +: ${user=admin} +: ${password=admin} + +check_parameter() { + if [ ! -x $CURLBIN ] + then + ha_log.sh err "Curl can't be found in normal place. Set CURLBIN to override the default value" + exit 1 + fi + + if [ -z $hmc_ipaddr ] + then + ha_log.sh err "The address of HMC web console is not specified" + exit 1 + fi +} + +HMCUSERNAME=$user +HMCPASSWORD=$password + +HMC_LOGIN_COMMAND="$CURLBIN -3 -k -c $COOKIEFILE -d user=$HMCUSERNAME -d password=$HMCPASSWORD -d lang=0 -d submit=Log+in " +HMC_LOGOUT_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d submit=Log+out " +HMC_TOC_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d form=2 " +HMC_POWERON_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d form=60 -d sp=255 -d is=0 -d om=4 -d id=1 -d ip=2 -d plt=1 -d pm=0 -d on=Save+settings+and+power+on " +HMC_POWERSTATE_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d form=60 " +HMC_POWEROFF_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d form=30 -d submit=Continue " +HMC_REBOOT_COMMAND="$CURLBIN -3 -k -b $COOKIEFILE -d form=74 -d submit=Continue " + +hmc_login() { + iamin=0 + while [ $iamin -eq 0 ]; do + $HMC_LOGIN_COMMAND https://$hmc_ipaddr/cgi-bin/cgi >/dev/null 2>&1 + $HMC_TOC_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null | grep -q "Too many users" + iamin=$? + sleep 2 + done +} +hmc_logout() { + $HMC_LOGOUT_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null +} + +hmc_reboot() { + check_parameter + $HMC_REBOOT_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null +} +hmc_poweron() { + r=1 + while [ 0 -ne $r ]; do + $HMC_POWERON_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null | grep -q "Operation completed successfully" + r=$? + done +} +hmc_poweroff() { + check_parameter + $HMC_POWEROFF_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null +} +hmc_powerstate() { + check_parameter + r=`$HMC_POWERSTATE_COMMAND https://$hmc_ipaddr/cgi-bin/cgi 2>/dev/null| grep "Current system power state:" | sed 's/<br>//g' | awk '{print $5}'` + echo $r +} + +hmc_poweroffon() { + check_parameter + hmc_poweroff + while [ 1 ]; do + r=`hmc_powerstate` + ha_log.sh debug "power state: $r" + if [ $r = "Off" ]; then + break + fi + sleep 5 + done + sleep 3 + hmc_poweron +} + +case $1 in +gethosts) + for h in $hostlist; do + echo $h + done + exit 0 + ;; +status) + if + ping -w1 -c1 "$hmc_ipaddr" 2>&1 + then + exit 0 + fi + exit 1 + ;; +getconfignames) + for f in hostlist hmc_ipaddr user password; do + echo $f + done + exit 0 + ;; +getinfo-devid) + echo "HMC web console STONITH device" + exit 0 + ;; +getinfo-devname) + echo "HMC web console STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "HMC web console based host power control" + echo "Use for i5, p5, pSeries and OpenPower systems that are managed via " + echo "web console through a direct connection to system's HMC port." + exit 0 + ;; +getinfo-devurl) + echo "http://www.ibm.com" + exit 0 + ;; +getinfo-xml) + cat << HMCXML +<parameters> + +<parameter name="hostlist" unique="1" required="1"> +<content type="string"/> +<shortdesc lang="en">Hostlist</shortdesc> +<longdesc lang="en"> +The list of hosts that the STONITH device controls +</longdesc> +</parameter> + +<parameter name="hmc_ipaddr" unique="1" required="1"> +<content type="string"/> +<shortdesc lang="en">HMC IPAddr</shortdesc> +<longdesc lang="en"> +The IP address of the HMC web console +</longdesc> +</parameter> + +<parameter name="user" unique="1" required="1"> +<content type="string"/> +<shortdesc lang="en">User</shortdesc> +<longdesc lang="en"> +User name to log into HMC web console +</longdesc> +</parameter> + +<parameter name="password" unique="1" required="1"> +<content type="string"/> +<shortdesc lang="en">Password</shortdesc> +<longdesc lang="en"> +The password of user name to log into HMC web console +</longdesc> +</parameter> + +</parameters> +HMCXML + exit 0 + ;; +esac + +case $1 in +on|off|reset|powerstate|poweroffon) + hmc_login + case $1 in + on) + hmc_poweron $hmc_ipaddr + ;; + off) + hmc_poweroff $hmc_ipaddr + ;; + reset) +# hmc_reboot $hmc_ipaddr + hmc_poweroffon $hmc_ipaddr + ;; + powerstate) + hmc_powerstate + ;; + poweroffon) + hmc_poweroffon + ;; + esac + hmc_logout + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/ibmrsa b/lib/plugins/stonith/external/ibmrsa new file mode 100644 index 0000000..7408465 --- /dev/null +++ b/lib/plugins/stonith/external/ibmrsa @@ -0,0 +1,157 @@ +#!/bin/sh +# +# Copyright (c) 2006 Dejan Muhamedagic <dmuhamedagic@at.ibm.com>, IBM Austria +# +# External STONITH module for IBM RSA adapters. +# External STONITH module for IBM BMC. +# This STONITH module depends on IBMmpcli. +# + +trap 'rm -f "$outf"' 0 +outf=`mktemp` || { + ha_log.sh err 'mktemp failed' + exit 1 +} + +chkmpcli() { + test -x /opt/IBMmpcli/bin/MPCLI.sh +} +mpcli() { + chkmpcli || { + ha_log.sh err "IBM mpcli not installed" + return 1 + } + if [ x = "x$ipaddr" -o x = "x$userid" -o x = "x$passwd" ] + then + ha_log.sh err "ipaddr, userid, or passwd missing; check configuration" + return 1 + fi + type=${type:-"ibm"} + + goodstg="SUCCESS" + failstg="FAILURE" + ( + echo "logonip -h $ipaddr -u $userid -p $passwd -t $type" + echo "outputfile $outf" + cat + ) | /opt/IBMmpcli/bin/MPCLI.sh | grep -w $goodstg >/dev/null 2>&1 + rc=$? + grep -w $failstg $outf >/dev/null + if [ $rc -eq 0 -a $? -eq 1 ]; then + return 0 + else + ha_log.sh err "MPCLI.sh failed: `cat $outf`" + return 1 + fi +} +ibmrsa_reboot() { + echo restart -now | mpcli +} +ibmrsa_poweron() { + echo poweron | mpcli +} +ibmrsa_poweroff() { + echo poweroff | mpcli +} +ibmrsa_status() { + echo | mpcli +} + +hostname=`echo ${hostname} | tr ',' ' '` + +case $1 in +gethosts) + echo $hostname + ;; +on) + ibmrsa_poweron + ;; +off) + ibmrsa_poweroff + ;; +reset) + ibmrsa_reboot + ;; +status) + ibmrsa_status + ;; +getconfignames) + for i in hostname ipaddr userid passwd type; do + echo $i + done + ;; +getinfo-devid) + echo "IBM MP STONITH device" + ;; +getinfo-devname) + echo "IBM MP STONITH device" + ;; +getinfo-devdescr) + echo "IBM MP host reboot/poweron/poweroff" + ;; +getinfo-devurl) + echo "http://www.ibm.com" + ;; +getinfo-xml) + cat <<EOF +<parameters> + +<parameter name="hostname" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostname +</shortdesc> +<longdesc lang="en"> +The hostname of the host to be managed by this STONITH device +</longdesc> +</parameter> + +<parameter name="ipaddr" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +IP Address +</shortdesc> +<longdesc lang="en"> +The IP address of the STONITH device +</longdesc> +</parameter> + +<parameter name="userid" unique="0" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Login +</shortdesc> +<longdesc lang="en"> +The username used to login into the STONITH device +</longdesc> +</parameter> + +<parameter name="passwd" unique="0" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Password +</shortdesc> +<longdesc lang="en"> +The password used to login into the STONITH device +</longdesc> +</parameter> + +<parameter name="type" unique="0" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Management processor type +</shortdesc> +<longdesc lang="en"> +The type of the management processor. Possible values are +"ibm" (default, typically used for RSA) and "ipmi" +(for IPMI compliant processors such as BMC). +</longdesc> +</parameter> + +</parameters> +EOF + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/ibmrsa-telnet b/lib/plugins/stonith/external/ibmrsa-telnet new file mode 100644 index 0000000..4d75d9a --- /dev/null +++ b/lib/plugins/stonith/external/ibmrsa-telnet @@ -0,0 +1,320 @@ +#!/usr/bin/python +# vim: set filetype=python +####################################################################### +# +# ibmrsa-telnet - External stonith plugin for HAv2 (http://linux-ha.org/wiki) +# Connects to IBM RSA Board via telnet and switches power +# of server appropriately. +# +# Author: Andreas Mock (andreas.mock@web.de) +# +# History: +# 2007-10-19 Fixed bad commandline handling in case of stonithing +# 2007-10-11 First release. +# +# Comment: Please send bug fixes and enhancements. +# I hope the functionality of communicating via telnet is encapsulated +# enough so that someone can use it for similar purposes. +# +# Description: IBM offers Remote Supervisor Adapters II for several +# servers. These RSA boards can be accessed in different ways. +# One of that is via telnet. Once logged in you can use 'help' to +# show all available commands. With 'power' you can reset, power on and +# off the controlled server. This command is used in combination +# with python's standard library 'telnetlib' to do it automatically. +# +# cib-snippet: Please see README.ibmrsa-telnet for examples. +# +# Copyright (c) 2007 Andreas Mock (andreas.mock@web.de) +# All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 or later of the GNU General Public +# License as published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# +####################################################################### +import sys +import os +import time +import telnetlib +import subprocess + +class TimeoutException(Exception): + def __init__(self, value=None): + Exception.__init__(self) + self.value = value + + def __str__(self): + return repr(self.value) + +class RSABoard(telnetlib.Telnet): + def __init__(self, *args, **kwargs): + telnetlib.Telnet.__init__(self, *args, **kwargs) + self._timeout = 10 + self._loggedin = 0 + self._history = [] + self._appl = os.path.basename(sys.argv[0]) + + def _get_timestamp(self): + ct = time.time() + msecs = (ct - long(ct)) * 1000 + return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(ct)), msecs) + + def write(self, buffer, nolog = False): + self._history.append(self._get_timestamp() + ': WRITE: ' + + (nolog and '******' or repr(buffer))) + telnetlib.Telnet.write(self, buffer) + + def expect(self, what, timeout=20): + line = telnetlib.Telnet.expect(self, what, timeout) + self._history.append(self._get_timestamp() + ': READ : ' + repr(line)) + if not line: + raise TimeoutException("Timeout while waiting for '%s'." % (what, )) + return line + + def login(self, user, passwd): + time.sleep(1) + line = self.expect(['\nlogin : ', '\nusername: '], self._timeout) + self.write(user) + self.write('\r') + line = self.expect(['\nPassword: ', '\npassword: '], self._timeout) + self.write(passwd, nolog = True) + self.write('\r') + line = self.expect(['\nsystem>', '> '], self._timeout) + + def reset(self): + self.write('power cycle\r') + line = self.expect(['\nok'], self._timeout) + line = self.expect(['\nsystem>', '> '], self._timeout) + + def on(self): + self.write('power on\r') + line = self.expect(['\nok'], self._timeout) + line = self.expect(['\nsystem>', '> '], self._timeout) + + def off(self): + self.write('power off\r') + line = self.expect(['\nok'], self._timeout) + line = self.expect(['\nsystem>', '> '], self._timeout) + + def exit(self): + self.write('exit\r') + + def get_history(self): + return "\n".join(self._history) + + +class RSAStonithPlugin: + def __init__(self): + # define the external stonith plugin api + self._required_cmds = \ + 'reset gethosts status getconfignames getinfo-devid ' \ + 'getinfo-devname getinfo-devdescr getinfo-devurl ' \ + 'getinfo-xml' + self._optional_cmds = 'on off' + self._required_cmds_list = self._required_cmds.split() + self._optional_cmds_list = self._optional_cmds.split() + + # who am i + self._appl = os.path.basename(sys.argv[0]) + + # telnet connection object + self._connection = None + + # the list of configuration names + self._confignames = ['nodename', 'ip_address', 'username', 'password'] + + # catch the parameters provided by environment + self._parameters = {} + for name in self._confignames: + try: + self._parameters[name] = os.environ.get(name, '').split()[0] + except IndexError: + self._parameters[name] = '' + + def _get_timestamp(self): + ct = time.time() + msecs = (ct - long(ct)) * 1000 + return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", + time.localtime(ct)), msecs) + + def _echo_debug(self, *args): + self.echo_log('debug', *args) + + def echo(self, *args): + what = ''.join([str(x) for x in args]) + sys.stdout.write(what) + sys.stdout.write('\n') + sys.stdout.flush() + self._echo_debug("STDOUT:", what) + + def echo_log(self, level, *args): + subprocess.call(('ha_log.sh', level) + args) + + def _get_connection(self): + if not self._connection: + c = RSABoard() + self._echo_debug("Connect to '%s'" % + (self._parameters['ip_address'],)) + c.open(self._parameters['ip_address']) + self._echo_debug("Connection established") + c.login(self._parameters['username'], + self._parameters['password']) + self._connection = c + + def _end_connection(self): + if self._connection: + self._connection.exit() + self._connection.close() + + def reset(self): + self._get_connection() + self._connection.reset() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Reset of node '%s' done" % + (self._parameters['nodename'],)) + return(0) + + def on(self): + self._get_connection() + self._connection.on() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Switched node '%s' ON" % + (self._parameters['nodename'],)) + return(0) + + def off(self): + self._get_connection() + self._connection.off() + self._end_connection() + self._echo_debug(self._connection.get_history()) + self.echo_log("info", "Switched node '%s' OFF" % + (self._parameters['nodename'],)) + return(0) + + def gethosts(self): + self.echo(self._parameters['nodename']) + return(0) + + def status(self): + self._get_connection() + self._end_connection() + self._echo_debug(self._connection.get_history()) + return(0) + + def getconfignames(self): + for name in ['nodename', 'ip_address', 'username', 'password']: + self.echo(name) + return(0) + + def getinfo_devid(self): + self.echo("External Stonith Plugin for IBM RSA Boards") + return(0) + + def getinfo_devname(self): + self.echo("External Stonith Plugin for IBM RSA Boards connecting " + "via Telnet") + return(0) + + def getinfo_devdescr(self): + self.echo("External stonith plugin for HAv2 which connects to " + "a RSA board on IBM servers via telnet. Commands to " + "turn on/off power and to reset server are sent " + "appropriately. " + "(c) 2007 by Andreas Mock (andreas.mock@web.de)") + return(0) + + def getinfo_devurl(self): + self.echo("http://www.ibm.com/Search/?q=remote+supervisor+adapter") + + def getinfo_xml(self): + info = """<parameters> + <parameter name="nodename" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">nodename to shoot</shortdesc> + <longdesc lang="en"> + Name of the node which has to be stonithed in case. + </longdesc> + </parameter> + <parameter name="ip_address" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">hostname or ip address of RSA</shortdesc> + <longdesc lang="en"> + Hostname or ip address of RSA board used to reset node. + </longdesc> + </parameter> + <parameter name="username" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">username to login on RSA board</shortdesc> + <longdesc lang="en"> + Username to login on RSA board. + </longdesc> + </parameter> + <parameter name="password" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">password to login on RSA board</shortdesc> + <longdesc lang="en"> + Password to login on RSA board. + </longdesc> + </parameter> + </parameters> + """ + self.echo(info) + return(0) + + def not_implemented(self, cmd): + self.echo_log("err", "Command '%s' not implemented." % (cmd,)) + return(1) + + def usage(self): + usage = "Call me with one of the allowed commands: %s, %s" % ( + ', '.join(self._required_cmds_list), + ', '.join(self._optional_cmds_list)) + return usage + + def process(self, argv): + self._echo_debug("========== Start =============") + if len(argv) < 1: + self.echo_log("err", 'At least one commandline argument required.') + return(1) + cmd = argv[0] + self._echo_debug("cmd:", cmd) + if cmd not in self._required_cmds_list and \ + cmd not in self._optional_cmds_list: + self.echo_log("err", "Command '%s' not supported." % (cmd,)) + return(1) + try: + cmd = cmd.lower().replace('-', '_') + func = getattr(self, cmd, self.not_implemented) + rc = func() + return(rc) + except Exception, args: + self.echo_log("err", 'Exception raised:', str(args)) + if self._connection: + self.echo_log("err", self._connection.get_history()) + self._connection.close() + return(1) + + +if __name__ == '__main__': + stonith = RSAStonithPlugin() + rc = stonith.process(sys.argv[1:]) + sys.exit(rc) diff --git a/lib/plugins/stonith/external/ipmi b/lib/plugins/stonith/external/ipmi new file mode 100644 index 0000000..abadd5a --- /dev/null +++ b/lib/plugins/stonith/external/ipmi @@ -0,0 +1,276 @@ +#!/bin/sh +# +# External STONITH module using IPMI. +# This modules uses uses the ipmitool program available from +# http://ipmitool.sf.net/ for actual communication with the +# managed device. +# +# Copyright (c) 2007 Martin Bene <martin.bene@icomedias.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# Initialization -- fix locale settings so we can parse output from +# binaries if we need it +LANG=C +LC_ALL=C + +RESET="power reset" +POWEROFF="power off" +POWERON="power on" +STATUS="power status" +IPMITOOL=${ipmitool:-"`which ipmitool 2>/dev/null`"} + +have_ipmi() { + test -x "${IPMITOOL}" +} + +# Wrapper function for ipmitool that sets the correct host IP address, +# username, and password, and invokes ipmitool with any arguments +# passed in +run_ipmitool() { + local ipmitool_opts privlvl="" + have_ipmi || { + ha_log.sh err "ipmitool not installed" + return 1 + } + if [ -z "${ipaddr}" -o -z "${userid}" -o -z "${passwd}" ]; then + ha_log.sh err "ipaddr, userid or passwd missing; check configuration" + return 1 + fi + + if [ -z "${interface}" ]; then + # default to "lan" interface + interface="lan" + fi + if [ -n "${priv}" ]; then + # default to "lan" interface + privlvl="-L $priv" + fi + + ipmitool_opts="-I ${interface} -H ${ipaddr} $privlvl" + + case "${passwd_method}" in + param|'') + passwd_method=param + M="-P" + ;; + env) + M="-E" + ;; + file) + M="-f" + ;; + *) + ha_log.sh err "invalid passwd_method: \"${passwd_method}\"" + return 1 + esac + + action="$*" + + if [ $passwd_method = env ] + then + IPMI_PASSWORD="${passwd}" ${IPMITOOL} $ipmitool_opts -U "${userid}" -E ${action} + else + ${IPMITOOL} $ipmitool_opts -U "${userid}" $M "${passwd}" ${action} + fi 2>&1 +} + +# Yet another convenience wrapper that invokes run_ipmitool, captures +# its output, logs the output, returns either 0 (on success) or 1 (on +# any error) +do_ipmi() { + if outp=`run_ipmitool $*`; then + ha_log.sh debug "ipmitool output: `echo $outp`" + return 0 + else + ha_log.sh err "error executing ipmitool: `echo $outp`" + return 1 + fi +} + +# Check if the managed node is powered on. To do so, issue the "power +# status" command. Should return either "Chassis Power is on" or +# "Chassis Power is off". +ipmi_is_power_on() { + local outp + outp=`run_ipmitool ${STATUS}` + case "${outp}" in + *on) + return 0 + ;; + *off) + return 1 + ;; + esac +} + + +case ${1} in +gethosts) + echo $hostname + exit 0 + ;; +on) + do_ipmi "${POWERON}" + exit + ;; +off) + do_ipmi "${POWEROFF}" + exit + ;; +reset) + if ipmi_is_power_on; then + do_ipmi "${RESET}" + else + do_ipmi "${POWERON}" + fi + exit + ;; +status) + # "status" reflects the status of the stonith _device_, not + # the managed node. Hence, only check if we can contact the + # IPMI device with "power status" command, don't pay attention + # to whether the node is in fact powered on or off. + do_ipmi "${STATUS}" + exit $? + ;; +getconfignames) + for i in hostname ipaddr userid passwd interface; do + echo $i + done + exit 0 + ;; +getinfo-devid) + echo "IPMI STONITH device" + exit 0 + ;; +getinfo-devname) + echo "IPMI STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "ipmitool based power management. Apparently, the power off" + echo "method of ipmitool is intercepted by ACPI which then makes" + echo "a regular shutdown. If case of a split brain on a two-node" + echo "it may happen that no node survives. For two-node clusters" + echo "use only the reset method." + exit 0 + ;; +getinfo-devurl) + echo "http://ipmitool.sf.net/" + exit 0 + ;; +getinfo-xml) + cat << IPMIXML +<parameters> +<parameter name="hostname" unique="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostname +</shortdesc> +<longdesc lang="en"> +The name of the host to be managed by this STONITH device. +</longdesc> +</parameter> + +<parameter name="ipaddr" unique="1"> +<content type="string" /> +<shortdesc lang="en"> +IP Address +</shortdesc> +<longdesc lang="en"> +The IP address of the STONITH device. +</longdesc> +</parameter> + +<parameter name="userid" unique="0"> +<content type="string" /> +<shortdesc lang="en"> +Login +</shortdesc> +<longdesc lang="en"> +The username used for logging in to the STONITH device. +</longdesc> +</parameter> + +<parameter name="passwd" unique="0"> +<content type="string" /> +<shortdesc lang="en"> +Password +</shortdesc> +<longdesc lang="en"> +The password used for logging in to the STONITH device. +</longdesc> +</parameter> + +<parameter name="passwd_method" unique="0"> +<content type="string" default="param"/> +<shortdesc lang="en"> +Method for passing passwd parameter +</shortdesc> +<longdesc lang="en"> +Method for passing the passwd parameter to ipmitool + param: pass as parameter (-P) + env: pass via environment (-E) + file: value of "passwd" is actually a file name, pass with (-f) +</longdesc> +</parameter> + +<parameter name="interface" unique="0"> +<content type="string" default="lan"/> +<shortdesc lang="en"> +IPMI interface +</shortdesc> +<longdesc lang="en"> +IPMI interface to use, such as "lan" or "lanplus". +</longdesc> +</parameter> + +<parameter name="priv" unique="0"> +<content type="string" default=""/> +<shortdesc lang="en"> +The privilege level of the user. +</shortdesc> +<longdesc lang="en"> +The privilege level of the user, for instance OPERATOR. If +unspecified the privilege level is ADMINISTRATOR. See +ipmitool(1) -L option for more information. +</longdesc> +</parameter> + +<parameter name="ipmitool" unique="0"> +<content type="string" default=""/> +<shortdesc lang="en"> +IPMI command(ipmitool) +</shortdesc> +<longdesc lang="en"> +Specify the full path to IPMI command. +</longdesc> +</parameter> + +</parameters> +IPMIXML + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/ippower9258.in b/lib/plugins/stonith/external/ippower9258.in new file mode 100755 index 0000000..6ae7e02 --- /dev/null +++ b/lib/plugins/stonith/external/ippower9258.in @@ -0,0 +1,316 @@ +#!/bin/sh +# +# External STONITH module using IP Power 9258 or compatible devices. +# +# Copyright (c) 2010 Helmut Weymann (Helmut (at) h-weymann (dot) de) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +# +# Basic commands & parameters independent from individual device + +DEVICE="IP Power 9258" +IPPowerOn="1" +IPPowerOff="0" +IPGetPower="Set.cmd?CMD=GetPower" +IPSetPower="Set.cmd?CMD=SetPower" +IPPort_name="P" +IPPort0=60 +HTTP_COMMAND="wget -q -O - --" +LOG_ERROR="ha_log.sh err" +LOG_WARNING="ha_log.sh warn" +LOG_INFO="ha_log.sh info" +LOG_DEBUG="ha_log.sh debug" +MY_COOKIES="cookies.txt" +MY_TEMPFILE="temp.htm" +PORT_STATUS="iocontrol.htm" +UNDEFINED_HOSTNAME="*not-defined*" + +# +# check MY_ROOT_PATH for IP Power 9258 and create it if necessary +MY_ROOT_PATH="@GLUE_STATE_DIR@/heartbeat/rsctmp/ippower9258" + +# +# script functions +# + +get_challenge() { + # + # device sends a challenge for md5 encryption of username, password and challenge + send_web_command - "http://$deviceip/" | grep Challenge | grep input | cut -d '"' -f 6 +} + +get_cookie_from_device(){ + # the form on the login page has these fields: + # Username, Password, Challenge, Response, ScreenWidth + # + challenge=`get_challenge` + response=`echo -n "$username$password$challenge" | md5sum | cut -b -32` + postdata="Username=$username&Password=&Challenge=&Response=$response&ScreenWidth=1024" + send_web_command " $MY_PATH/$MY_TEMPFILE --post-data=$postdata" "http://$deviceip/tgi/login.tgi" + if grep -qs "Invalid User name or Password" $MY_PATH/$MY_TEMPFILE + then + $LOG_ERROR "Login to device $deviceip failed." + $LOG_ERROR "Received Challenge = <<<$challenge>>>." + $LOG_ERROR "Sent postdata = <<<$postdata>>>." + exit 1 + fi +} + +get_data_from_device() { + # If successful all device info is available in MY_PATH + rm -f "$MY_PATH/$PORT_STATUS" + send_web_command "$MY_PATH/$PORT_STATUS" "http://$deviceip/$PORT_STATUS" + if grep -qs "Cookie Time Out" $MY_PATH/$PORT_STATUS + then + $LOG_ERROR "received no port data from $deviceip (Cookie Time Out)" + exit 1 + fi +} + +send_http_request() { + # ececution of http commands supported by the device + $HTTP_COMMAND "http://$username:$password@$deviceip/$1" +} + +send_web_command(){ + # ececution of web commands through the web-interface + WEB_COMMAND="wget -q --keep-session-cookies" + WEB_COMMAND="$WEB_COMMAND --load-cookies $MY_PATH/$MY_COOKIES" + WEB_COMMAND="$WEB_COMMAND --save-cookies $MY_PATH/$MY_COOKIES" + $WEB_COMMAND -O $1 -- $2 +} + +name2port() { + local name=$1 + local i=$IPPort0 + for h in $device_hostlist ; do + if [ $h = $name ]; then + echo $IPPort_name$i + return + fi + i=`expr $i + 1` + done + echo "invalid" +} + +set_port() { + # + # port status is always set. Even if requested status is current status. + # host status is not considered. + local host=$1 + local requested_status=$2 # 0 or 1 + local port=`name2port $host` + if [ "$port" = "invalid" ] + then + $LOG_ERROR "Host $host is not in hostlist ($hostlist) for $deviceip." + exit 1 + fi + ret=`send_http_request "$IPSetPower+$port=$requested_status" | cut -b 11` + if [ "$ret" != "$requested_status" ] + then + $LOG_ERROR "$DEVICE at $deviceip responds with wrong status $ret for host $host at port $port." + exit 1 + fi + return 0 +} + +build_device_hostlist() { + # + # hostnames are available from http://$deviceip/iocontrol.htm" + # check for number of ports + # + device_hostlist=$( + w3m -dump $MY_PATH/$PORT_STATUS | grep 'Power[1-8]' | + sed 's/[^[]*\[//;s/\].*//;s/ *//' | + while read h; do + [ -z "$h" ] && + echo $UNDEFINED_HOSTNAME || + echo $h + done + ) + if [ x = x"$device_hostlist" ]; then + $LOG_ERROR "cannot get hostlist for $deviceip" + exit 1 + fi + $LOG_DEBUG "Got new hostlist ($device_hostlist) from $deviceip" +} + +filter_device_hostlist() { + # check the given hostlist against the device hostlist + local host + for host in $device_hostlist; do + [ "$host" != "$UNDEFINED_HOSTNAME" ] && + echo $host + done +} + +check_hostlist() { + # check the given hostlist against the device hostlist + local cnt=`echo "$hostlist" | wc -w` + local cnt2=0 + local host + for host in $hostlist; do + if [ `name2port $host` != "invalid" ]; then + cnt2=$((cnt2+1)) + else + $LOG_ERROR "host $host not defined at $deviceip" + fi + done + [ $cnt -ne $cnt2 ] && + exit 1 +} + +get_http_status() { + pattern="P60=[01],P61=[01],P62=[01],P63=[01],P64=[01],P65=[01],P66=[01],P67=[01]" + ret=`send_http_request "$IPGetPower" | grep $pattern` + if [ "X$ret" = "X" ] + then + $LOG_ERROR "$DEVICE at $deviceip returns invalid or no string." + exit 1 + fi +} + +hostlist=`echo $hostlist | tr ',' ' '` +case $1 in +gethosts|on|off|reset|status) + # need environment from stonithd + # and device information from individual device + # + # default device username is admin + # IP Power 9258 does not allow user management. + # + if [ "X$username" = "X" ] + then + username="admin" + fi + + mkdir -p $MY_ROOT_PATH + tmp_path="$deviceip" # ensure a simple unique pathname + MY_PATH="$MY_ROOT_PATH/$tmp_path" + mkdir -p $MY_PATH + get_cookie_from_device + get_data_from_device + build_device_hostlist + if [ "X$hostlist" = "X" ]; then + hostlist="`filter_device_hostlist`" + else + check_hostlist + fi + ;; +*) + # the client is asking for meta-data + ;; +esac + +target=`echo $2 | sed 's/[.].*//'` +# the necessary actions for stonithd +case $1 in +gethosts) + echo $hostlist + ;; +on) + set_port $target $IPPowerOn + ;; +off) + set_port $target $IPPowerOff + ;; +reset) + set_port $target $IPPowerOff + sleep 5 + set_port $target $IPPowerOn + ;; +status) + # werify http command interface + get_http_status + ;; +getconfignames) + # return all the config names + for ipparam in deviceip username password hostlist + do + echo $ipparam + done + ;; +getinfo-devid) + echo "IP Power 9258" + ;; +getinfo-devname) + echo "IP Power 9258 power switch" + ;; +getinfo-devdescr) + echo "Power switch IP Power 9258 with 4 or 8 power outlets." + echo "WARNING: It is different from IP Power 9258 HP" + ;; +getinfo-devurl) + echo "http://www.aviosys.com/manual.htm" + ;; +getinfo-xml) + cat << IPPOWERXML +<parameters> +<parameter name="deviceip" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +IP address or hostname of the device. +</shortdesc> +<longdesc lang="en"> +The IP Address or the hostname of the device. +</longdesc> +</parameter> + +<parameter name="password" unique="0" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Password +</shortdesc> +<longdesc lang="en"> +The password to log in with. +</longdesc> +</parameter> + +<parameter name="hostlist" unique="0" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Hostlist +</shortdesc> +<longdesc lang="en"> +The list of hosts that the device controls. +If you leave this list empty, we will retrieve the hostnames from the device. +</longdesc> +</parameter> + +<parameter name="username" unique="0" required="0"> +<content type="string" default="admin"/> +<shortdesc lang="en"> +Account Name +</shortdesc> +<longdesc lang="en"> +The user to log in with. +</longdesc> +</parameter> + +</parameters> +IPPOWERXML + ;; +*) + $LOG_ERROR "Unexpected command $1 for $DEVICE at $deviceip." + exit 1; + ;; +esac diff --git a/lib/plugins/stonith/external/kdumpcheck.in b/lib/plugins/stonith/external/kdumpcheck.in new file mode 100644 index 0000000..7f3f752 --- /dev/null +++ b/lib/plugins/stonith/external/kdumpcheck.in @@ -0,0 +1,274 @@ +#!/bin/sh +# +# External STONITH module to check kdump. +# +# Copyright (c) 2008 NIPPON TELEGRAPH AND TELEPHONE CORPORATION +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +SSH_COMMAND="@SSH@ -q -x -o PasswordAuthentication=no -o StrictHostKeyChecking=no -n" +#Set default user name. +USERNAME="kdumpchecker" +#Initialize identity file-path options for ssh command +IDENTITY_OPTS="" + +#Rewrite the hostlist to accept "," as a delimeter for hostnames too. +hostlist=`echo ${hostlist} | tr ',' ' '` + +## +# Check the parameter hostlist is set or not. +# If not, exit with 6 (ERR_CONFIGURED). +## +check_hostlist() { + if [ -z "${hostlist}" ]; then + ha_log.sh err "hostlist is empty" + exit 6 #ERR_CONFIGURED + fi +} + +## +# Set kdump check user name to USERNAME. +# always return 0. +## +get_username() { + kdump_conf="/etc/kdump.conf" + + if [ ! -f "${kdump_conf}" ]; then + ha_log.sh debug "${kdump_conf} doesn't exist" + return 0 + fi + + tmp="" + while read config_opt config_val; do + if [ "${config_opt}" = "kdump_check_user" ]; then + tmp="${config_val}" + fi + done < "${kdump_conf}" + if [ -n "${tmp}" ]; then + USERNAME="${tmp}" + fi + + ha_log.sh debug "kdump check user name is ${USERNAME}." +} + +## +# Check the specified or default identity file exists or not. +# If not, exit with 6 (ERR_CONFIGURED). +## +check_identity_file() { + IDENTITY_OPTS="" + if [ -n "${identity_file}" ]; then + if [ ! -f "${identity_file}" ]; then + ha_log.sh err "${identity_file} doesn't exist." + exit 6 #ERR_CONFIGURED + fi + IDENTITY_OPTS="-i ${identity_file}" + else + flg_file_exists=0 + homedir=`eval echo "~${USERNAME}"` + for filename in "${homedir}/.ssh/id_rsa" \ + "${homedir}/.ssh/id_dsa" \ + "${homedir}/.ssh/identity" + do + if [ -f "${filename}" ]; then + flg_file_exists=1 + IDENTITY_OPTS="${IDENTITY_OPTS} -i ${filename}" + fi + done + if [ ${flg_file_exists} -eq 0 ]; then + ha_log.sh err "${USERNAME}'s identity file for ssh command" \ + " doesn't exist." + exit 6 #ERR_CONFIGURED + fi + fi +} + +## +# Check the user to check doing kdump exists or not. +# If not, exit with 6 (ERR_CONFIGURED). +## +check_user_existence() { + + # Get kdump check user name and check whether he exists or not. + grep -q "^${USERNAME}:" /etc/passwd > /dev/null 2>&1 + ret=$? + if [ ${ret} != 0 ]; then + ha_log.sh err "user ${USERNAME} doesn't exist." \ + "please confirm \"kdump_check_user\" setting in /etc/kdump.conf." \ + "(default user name is \"kdumpchecker\")" + exit 6 #ERR_CONFIGURED + fi +} + +## +# Check the target node is kdumping or not. +# arg1 : target node name. +# ret : 0 -> the target is kdumping. +# : 1 -> the target is _not_ kdumping. +# : else -> failed to check. +## +check_kdump() { + target_node="$1" + + # Get kdump check user name. + get_username + check_user_existence + exec_cmd="${SSH_COMMAND} -l ${USERNAME}" + + # Specify kdump check user's identity file for ssh command. + check_identity_file + exec_cmd="${exec_cmd} ${IDENTITY_OPTS}" + + # Now, check the target! + # In advance, Write the following setting at the head of + # kdump_check_user's public key in authorized_keys file on target node. + # command="test -s /proc/vmcore", \ + # no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty + ha_log.sh debug "execute the command [${exec_cmd} ${target_node}]." + ${exec_cmd} ${target_node} > /dev/null 2>&1 + ret=$? + ha_log.sh debug "the command's result is ${ret}." + + #ret -> 0 : vmcore file's size is not zero. the node is kdumping. + #ret -> 1 : the node is _not_ kdumping (vmcore didn't exist or + # its size is zero). It still needs to be STONITH'ed. + #ret -> 255 : ssh command is failed. + # else : Maybe command strings in authorized_keys is wrong... + return ${ret} +} + +### +# +# Main function. +# +### +case $1 in +gethosts) + check_hostlist + for hostname in ${hostlist} ; do + echo "${hostname}" + done + exit 0 + ;; +on) + # This plugin does only check whether a target node is kdumping or not. + exit 1 + ;; +reset|off) + check_hostlist + ret=1 + h_target=`echo $2 | tr A-Z a-z` + for hostname in ${hostlist} + do + hostname=`echo $hostname | tr A-Z a-z` + if [ "${hostname}" != "$h_target" ]; then + continue + fi + while [ 1 ] + do + check_kdump "$2" + ret=$? + if [ ${ret} -ne 255 ]; then + exit ${ret} + fi + #255 means ssh command itself is failed. + #For example, connection failure as if network doesn't start yet + #in 2nd kernel on the target node. + #So, retry to check after a little while. + sleep 1 + done + done + exit ${ret} + ;; +status) + check_hostlist + for hostname in ${hostlist} + do + if ping -w1 -c1 "${hostname}" 2>&1 | grep "unknown host" + then + exit 1 + fi + done + get_username + check_user_existence + check_identity_file + exit 0 + ;; +getconfignames) + echo "hostlist identity_file" + exit 0 + ;; +getinfo-devid) + echo "kdump check STONITH device" + exit 0 + ;; +getinfo-devname) + echo "kdump check STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "ssh-based kdump checker" + echo "To check whether a target node is dumping or not." + exit 0 + ;; +getinfo-devurl) + echo "kdump -> http://lse.sourceforge.net/kdump/" + echo "ssh -> http://openssh.org" + exit 0 + ;; +getinfo-xml) + cat << SSHXML +<parameters> +<parameter name="hostlist" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostlist +</shortdesc> +<longdesc lang="en"> +The list of hosts that the STONITH device controls +</longdesc> +</parameter> + +<parameter name="identity_file" unique="1" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Identity file's full path for kdump check user +</shortdesc> +<longdesc lang="en"> +The full path of kdump check user's identity file for ssh command. +The identity in the specified file have to be restricted to execute +only the following command. +"test -s /proc/vmcore" +Default: kdump check user's default identity file path. +NOTE: You can specify kdump check user name in /etc/kdump.conf. + The parameter name is "kdump_check_user". + Default user is "kdumpchecker". +</longdesc> +</parameter> + +</parameters> +SSHXML + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/libvirt b/lib/plugins/stonith/external/libvirt new file mode 100644 index 0000000..494b048 --- /dev/null +++ b/lib/plugins/stonith/external/libvirt @@ -0,0 +1,298 @@ +#!/bin/sh +# +# External STONITH module for a libvirt managed hypervisor (kvm/Xen). +# Uses libvirt as a STONITH device to control guest. +# +# Copyright (c) 2010 Holger Teutsch <holger.teutsch@web.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# start a domain +libvirt_start() { + out=$($VIRSH -c $hypervisor_uri start $domain_id 2>&1) + if [ $? -eq 0 ] + then + ha_log.sh notice "Domain $domain_id was started" + return 0 + fi + + $VIRSH -c $hypervisor_uri dominfo $domain_id 2>&1 | + egrep -q '^State:.*(running|idle)|already active' + if [ $? -eq 0 ] + then + ha_log.sh notice "Domain $domain_id is already active" + return 0 + fi + + ha_log.sh err "Failed to start domain $domain_id" + ha_log.sh err "$out" + return 1 +} +# reboot a domain +# return +# 0: success +# 1: error +libvirt_reboot() { + local rc out + out=$($VIRSH -c $hypervisor_uri reboot $domain_id 2>&1) + rc=$? + if [ $rc -eq 0 ] + then + ha_log.sh notice "Domain $domain_id was rebooted" + return 0 + fi + ha_log.sh err "Failed to reboot domain $domain_id (exit code: $rc)" + ha_log.sh err "$out" + return 1 +} + +# stop a domain +# return +# 0: success +# 1: error +# 2: was already stopped +libvirt_stop() { + out=$($VIRSH -c $hypervisor_uri destroy $domain_id 2>&1) + if [ $? -eq 0 ] + then + ha_log.sh notice "Domain $domain_id was stopped" + return 0 + fi + + $VIRSH -c $hypervisor_uri dominfo $domain_id 2>&1 | + egrep -q '^State:.*shut off|not found|not running' + if [ $? -eq 0 ] + then + ha_log.sh notice "Domain $domain_id is already stopped" + return 2 + fi + + ha_log.sh err "Failed to stop domain $domain_id" + ha_log.sh err "$out" + return 1 +} + +# get status of stonith device (*NOT* of the domain). +# If we can retrieve some info from the hypervisor +# the stonith device is OK. +libvirt_status() { + out=$($VIRSH -c $hypervisor_uri version 2>&1) + if [ $? -eq 0 ] + then + return 0 + fi + + ha_log.sh err "Failed to get status for $hypervisor_uri" + ha_log.sh err "$out" + return 1 +} + +# check config and set variables +# does not return on error +libvirt_check_config() { + VIRSH=`which virsh 2>/dev/null` + + if [ ! -x "$VIRSH" ] + then + ha_log.sh err "virsh not installed" + exit 1 + fi + + if [ -z "$hostlist" -o -z "$hypervisor_uri" ] + then + ha_log.sh err "hostlist or hypervisor_uri missing; check configuration" + exit 1 + fi + + case "$reset_method" in + power_cycle|reboot) : ;; + *) + ha_log.sh err "unrecognized reset_method: $reset_method" + exit 1 + ;; + esac +} + +# set variable domain_id for the host specified as arg +libvirt_set_domain_id () +{ + for h in $hostlist + do + case $h in + $1:*) + domain_id=`expr $h : '.*:\(.*\)'` + return + ;; + + $1) + domain_id=$1 + return + esac + done + + ha_log.sh err "Should never happen: Called for host $1 but $1 is not in $hostlist." + exit 1 +} + +libvirt_info() { +cat << LVIRTXML +<parameters> +<parameter name="hostlist" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +List of hostname[:domain_id].. +</shortdesc> +<longdesc lang="en"> +List of controlled hosts: hostname[:domain_id].. +The optional domain_id defaults to the hostname. +</longdesc> +</parameter> + +<parameter name="hypervisor_uri" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hypervisor URI +</shortdesc> +<longdesc lang="en"> +URI for connection to the hypervisor. +driver[+transport]://[username@][hostlist][:port]/[path][?extraparameters] +e.g. +qemu+ssh://my_kvm_server.mydomain.my/system (uses ssh for root) +xen://my_kvm_server.mydomain.my/ (uses TLS for client) + +virsh must be installed (e.g. libvir-client package) and access control must +be configured for your selected URI. +</longdesc> +</parameter> + +<parameter name="reset_method" required="0"> +<content type="string" default="power_cycle"/> +<shortdesc lang="en"> +How to reset a guest. +</shortdesc> +<longdesc lang="en"> +A guest reset may be done by a sequence of off and on commands +(power_cycle) or by the reboot command. Which method works +depend on the hypervisor and guest configuration management. +</longdesc> +</parameter> +</parameters> +LVIRTXML +exit 0 +} + +############# +# Main code # +############# + +# don't fool yourself when testing with stonith(8) +# and transport ssh +unset SSH_AUTH_SOCK + +# support , as a separator as well +hostlist=`echo $hostlist| sed -e 's/,/ /g'` + +reset_method=${reset_method:-"power_cycle"} + +case $1 in + gethosts) + hostnames=`echo $hostlist|sed -e 's/:[^ ]*//g'` + for h in $hostnames + do + echo $h + done + exit 0 + ;; + + on) + libvirt_check_config + libvirt_set_domain_id $2 + + libvirt_start + exit $? + ;; + + off) + libvirt_check_config + libvirt_set_domain_id $2 + + libvirt_stop + [ $? = 1 ] && exit 1 + exit 0 + ;; + + reset) + libvirt_check_config + libvirt_set_domain_id $2 + + if [ "$reset_method" = "power_cycle" ]; then + libvirt_stop + [ $? = 1 ] && exit 1 + sleep 2 + libvirt_start + else + libvirt_reboot + fi + exit $? + ;; + + status) + libvirt_check_config + libvirt_status + exit $? + ;; + + getconfignames) + echo "hostlist hypervisor_uri reboot_method" + exit 0 + ;; + + getinfo-devid) + echo "libvirt STONITH device" + exit 0 + ;; + + getinfo-devname) + echo "libvirt STONITH external device" + exit 0 + ;; + + getinfo-devdescr) + echo "libvirt-based host reset for Xen/KVM guest domain through hypervisor" + exit 0 + ;; + + getinfo-devurl) + echo "http://libvirt.org/uri.html http://linux-ha.org/wiki" + exit 0 + ;; + + getinfo-xml) + libvirt_info + echo 0; + ;; + + *) + exit 1 + ;; +esac + +# vi:et:ts=4:sw=4 diff --git a/lib/plugins/stonith/external/nut b/lib/plugins/stonith/external/nut new file mode 100644 index 0000000..9e51bb8 --- /dev/null +++ b/lib/plugins/stonith/external/nut @@ -0,0 +1,302 @@ +#!/bin/sh + +# External STONITH module that uses the NUT daemon to control an external UPS. +# See the comments below, and the various NUT man pages, for how this +# script works. It should work unchanged with most modern "smart" APC UPSes in +# a Redhat/Fedora/RHEL-style distribution with the nut package installed. + +# Author: William Seligman <seligman@nevis.columbia.edu> +# License: GPLv2 + +# As you're designing your UPS and STONITH set-up, it may help to consider that +# there can be potentially three computers involved: +# 1) the machine running this STONITH module; +# 2) the machine being controlled by this STONITH module ($hostname); +# 3) the machine that can send commands to the UPS. + +# On my cluster, all the UPSes have SNMP smartcards, so every host can communicate +# with every UPS; in other words, machines (1) and (3) are the same. If your UPSes +# are controlled via serial or USB connections, then you might have a +# situation in which $hostname is plugged into a UPS, which has a serial connection +# to some master "power-control" computer, and can potentially be STONITHed +# by any other machine in your cluster. + +# In general, you'll probably need the nut daemon running on both the hosts (1) and +# (3) in the above list. The NUT daemon will also have to run on (2) if you want the +# reset command to gracefully reboot $hostname. + +# The NUT command default locations. In the RHEL-type nut packages, these binaries +# are in /usr/bin. +RHELUPSCMD="/usr/bin/upscmd" +RHELUPSC="/usr/bin/upsc" + +# Defaults for APC smart UPSes: + +# Reset = reboot $hostname; this will be a graceful reboot if the host +# is running NUT and monitoring $ups. +APCRESET="shutdown.return" + +# Poweroff = turn off $hostname immediately by cutting the power on $ups. +# For a graceful shutdown, use shutdown.stayoff instead of load.off, +# but it might take a few minutes to shutdown in this way. +APCPOWEROFF="load.off" + +# Poweron = turn on the power to $ups, which will presumably turn on $hostname. +# (Did you set $hostname's BIOS to boot up on AC power restore, as opposed to +# "last state"?) +APCPOWERON="load.on" + +# Status = returns a short string with the $ups status; OL = on-line, OFF = off-line, etc. +APCSTATUSVAR="ups.status" + + +# Stick in the defaults, if needed. +if [ -z "${poweron}" ]; then + poweron=${APCPOWERON} +fi +if [ -z "${poweroff}" ]; then + poweroff=${APCPOWEROFF} +fi +if [ -z "${reset}" ]; then + reset=${APCRESET} +fi +if [ -z "${statusvar}" ]; then + statusvar=${APCSTATUSVAR} +fi +if [ -z "${upscmd}" ]; then + upscmd=${RHELUPSCMD} +fi +if [ -z "${upsc}" ]; then + upsc=${RHELUPSC} +fi + + +# Define the command to fetch the UPS status. +STATUSCMD="${upsc} ${ups} ${statusvar}" + +usage() { + echo "Usage: $0 {on|off|reset|status|gethosts|getconfignames|getinfo-devid|getinfo-devname|getinfo-devdescr|getinfo-devurl|getinfo-xml}" +} + +# Can we find the NUT binary? +have_nut() { + test -x "${upscmd}" +} +have_upsc() { + test -x "${upsc}" +} + +do_nut() { + have_nut || { + echo "Can't find NUT upscmd command" + return 1 + } + if [ -z "${username}" -o -z "${password}" -o -z "${ups}" ]; then + echo "username, password or ups name missing; check configuration" + return 1 + fi + # Execute the command given in argument 1. + ${upscmd} -u ${username} -p ${password} ${ups} ${1} || { + echo "error executing nut command" + return 1 + } +} + +case ${1} in +gethosts) + echo ${hostname} + exit 0 + ;; +on) + result=1 + do_nut "${poweron}" + result=$? + exit ${result} + ;; +off) + result=1 + do_nut "${poweroff}" + result=$? + exit ${result} + ;; +reset) + result=1 + do_nut "${reset}" + result=$? + exit $result + ;; +status) + have_upsc || { + echo "Can't find NUT upsc command" + exit 1 + } + ${STATUSCMD} + exit $? + ;; +getconfignames) + echo "hostname ups username password poweron poweroff reset statusvar upscmd upsc" + exit 0 + ;; +getinfo-devid) + echo "NUT STONITH device" + exit 0 + ;; +getinfo-devname) + echo "NUT STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "A STONITH device based on NUT (Network UPS Tools)." + echo " " + echo "For this STONITH script to work, the following conditions have" + echo "to be met:" + echo " " + echo "- NUT has to be installed on both the host running this script" + echo " and the host that controls the UPS (on RHEL systems, NUT is" + echo " in packages nut and nut-client) and the nut daemon services" + echo " (normally called the ups or upsd service) must be running" + echo " on both systems." + echo " " + echo "- The UPS name has to be defined in ups.conf on the host" + echo " that controls the UPS." + echo " " + echo "- The username/password to access the UPS must be defined in" + echo " upsd.users on the host that controls the UPS, with the instcmds" + echo " for poweron, poweroff, and reset allowed." + echo " " + echo "- The host that is running this script must be allowed access" + echo " via upsd.conf and upsd.users on the host the controls the UPS." + echo " " + echo "On RHEL systems, the files listed above are in /etc/ups." + echo " " + echo "The defaults will probably work with APC UPS devices. It might" + echo "work on others; 'upscmd -l (ups)' and 'upsc (ups)' will list" + echo "the commands and variables, and you can change the values" + echo "for poweron, poweroff, reset, and statusvar to suit your UPS." + echo "Change upscmd and upsc if your NUT binaries are not in /usr/bin." + exit 0 + ;; +getinfo-devurl) + echo "http://www.networkupstools.org/" + exit 0 + ;; +getinfo-xml) +cat << nutXML +<parameters> + +<parameter name="hostname" unique="1" required="1"> +<content type="string" default="" /> +<shortdesc lang="en">Hostname</shortdesc> +<longdesc lang="en"> +The name of the host to be managed by this STONITH device. +The nut daemon must be running on the host controllng the +UPS _and_ on the host running this script; this script does +not start/stop the daemons for you. +</longdesc> +</parameter> + +<parameter name="ups" required="1"> +<content type="string" default="" /> +<shortdesc lang="en">UPS name</shortdesc> +<longdesc lang="en"> +The name of the UPS as defined in ups.conf on the host +controlling the UPS. The format for this option is +upsname[@controlhost[:port]]. The default controlhost is +"localhost". +</longdesc> +</parameter> + +<parameter name="username" required="1"> +<content type="string" default="" /> +<shortdesc lang="en">Username</shortdesc> +<longdesc lang="en"> +The username used for accessing the UPS. This is defined in +upsd.conf on the host controlling the UPS. +</longdesc> +</parameter> + +<parameter name="password" required="1"> +<content type="string" default="" /> +<shortdesc lang="en">Password</shortdesc> +<longdesc lang="en"> +The password used for logging in to the UPS for the host +controlling the UPS, as defined in upsd.conf on that host. +</longdesc> +</parameter> + +<parameter name="poweron"> +<content type="string" default="$APCPOWERON" /> +<shortdesc lang="en">UPS Power On command</shortdesc> +<longdesc lang="en"> +The NUT hardware command to turn on the UPS. The default +should work for most "smart" APC UPSes. For a list of +commands that your UPS can support, type 'upscmd -l (ups)' +on the command line.</longdesc> +</parameter> + +<parameter name="poweroff"> +<content type="string" default="$APCPOWEROFF" /> +<shortdesc lang="en">UPS Power Off command</shortdesc> +<longdesc lang="en"> +The NUT hardware command to turn off the UPS. On most APC +"smart" UPSes, the command shutdown.stayoff will result +in a graceful shutdown, provided the host is running the +nut daemon, but this might take a few minutes; load.off +will cut the power immediately. For a list of commands that +your UPS can support, type 'upscmd -l (ups)' on the command +line. +</longdesc> +</parameter> + +<parameter name="reset"> +<content type="string" default="$APCRESET" /> +<shortdesc lang="en">UPS Reset command</shortdesc> +<longdesc lang="en"> +The NUT hardware command to reset the host. On most APC +"smart" UPSes, the command shutdown.return will result +in a graceful shutdown, with power restored after perhaps +a short interval. For a list of commands that your UPS can + support, type 'upscmd -l (ups)' on the command line. +</longdesc> +</parameter> + +<parameter name="statusvar"> +<content type="string" default="$APCSTATUSVAR" /> +<shortdesc lang="en">UPS Status variable</shortdesc> +<longdesc lang="en"> +The NUT variable that returns the status of the UPS. On APC +UPSes, the value of ups.status will be "OL" if the UPS is +"on-line." For a list of variables that your UPS supports, +type 'upsc (ups)' on the command line. +</longdesc> +</parameter> + +<parameter name="upscmd"> +<content type="string" default="$RHELUPSCMD" /> +<shortdesc lang="en">upscmd binary location</shortdesc> +<longdesc lang="en"> +The full path to the NUT binary command 'upscmd'. On RHEL +systems with the nut RPM installed, this location is +/usr/bin/upscmd. +</longdesc> +</parameter> + +<parameter name="upsc"> +<content type="string" default="$RHELUPSC" /> +<shortdesc lang="en">upsc binary location</shortdesc> +<longdesc lang="en"> +The full path to the NUT binary command 'upsc'. On RHEL +systems with the nut RPM installed, this location is +/usr/bin/upsc. +</longdesc> +</parameter> + +</parameters> +nutXML +exit 0 +;; +*) + usage + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/rackpdu b/lib/plugins/stonith/external/rackpdu new file mode 100644 index 0000000..7d0e20b --- /dev/null +++ b/lib/plugins/stonith/external/rackpdu @@ -0,0 +1,280 @@ +#!/bin/sh +# +# External STONITH module for APC Switched Rack PDU +# +# Copyright (c) 2008 Sergey Maznichenko <msergeyb@gmail.com> <inbox@it-consultant.su> +# Version 1.2 +# +# See http://www.it-consultant.su/rackpdu +# for additional information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +SWITCH_ON="1" +SWITCH_OFF="2" +SWITCH_RESET="3" + +DEFAULT_NAMES_OID=".1.3.6.1.4.1.318.1.1.12.3.3.1.1.2" +DEFAULT_COMMAND_OID=".1.3.6.1.4.1.318.1.1.12.3.3.1.1.4" + +if [ -z "$oid" ]; then + oid=$DEFAULT_COMMAND_OID +fi + +if [ -z "$names_oid" ]; then + names_oid=$DEFAULT_NAMES_OID +fi + +if [ -z "$outlet_config" ]; then + outlet_config="none" +fi + +GetOutletNumber() { + local nodename=$1 + + if [ "$outlet_config" != "none" ]; then + # Get outlet number from file + + if [ -f "$outlet_config" ]; then + local outlet_num=`grep $nodename $outlet_config | tr -d ' ' | cut -f2 -d'='` + if [ -z "$outlet_num" ]; then + ha_log.sh err "Outlet number not found for node $nodename. Check configuration file $outlet_config" + return 0 + fi + return $outlet_num + else + ha_log.sh err "File $outlet_config not found." + return 0 + fi + else + # Get outlet number from device + + local outlet_num=1 + local snmp_result + snmp_result=`snmpwalk -v1 -c $community $pduip $names_oid 2>&1` + if [ $? -ne 0 ]; then + ha_log.sh err "snmpwalk $community $pduip $names_oid failed. Result: $snmp_result" + return 0 + fi + + local names + names=`echo "$snmp_result" | cut -f2 -d'"' | tr ' ' '_' | tr '\012' ' '` + for name in $names; do + if [ "$name" != "$nodename" ]; then + local outlet_num=`expr $outlet_num + 1` + continue + fi + + return $outlet_num + done + + ha_log.sh err "Outlet number not found for node $nodename. Result: $snmp_result" + return 0 + fi +} + +SendCommand() { + + local host=$1 + local command=$2 + + GetOutletNumber $host + local outlet=$? + + if [ $outlet -gt 0 ]; then + local set_result + set_result=`snmpset -v1 -c $community $pduip $oid.$outlet i $command 2>&1` + if [ $? -ne 0 ]; then + ha_log.sh err "Write SNMP to $pduip value $oid.$outlet=$command failed. Result: $set_result" + return 1 + fi + if echo "$set_result" | grep -qs "Timeout"; then + ha_log.sh err "Write SNMP to $pduip value $oid.$outlet=$command timed out. Result: $set_result" + return 1 + fi + return 0 + else + return 1 + fi +} + +hostlist=`echo $hostlist | tr ',' ' '` +incommand=$1 +innode=$2 + +case $incommand in +gethosts) + if [ "$hostlist" = "AUTO" ]; then + snmp_result=`snmpwalk -v1 -c $community $pduip $names_oid 2>&1` + if [ $? -ne 0 ]; then + ha_log.sh err "snmpwalk $community $pduip $names_oid failed. Result: $snmp_result" + exit 1 + fi + if echo "$snmp_result" | grep -qs "Timeout"; then + ha_log.sh err "snmpwalk $community $pduip $names_oid timed out. Result: $snmp_result" + exit 1 + else + hostlist=`echo "$snmp_result" | cut -f2 -d'"' | tr ' ' '_' | tr '\012' ' '` + fi + fi + + for h in $hostlist ; do + echo $h + done + + exit 0 + ;; +on) + if + SendCommand $innode $SWITCH_ON + then + exit 0 + else + exit 1 + fi + ;; +off) + if + SendCommand $innode $SWITCH_OFF + then + exit 0 + else + exit 1 + fi + ;; +reset) + if + SendCommand $innode $SWITCH_RESET + then + exit 0 + else + exit 1 + fi + ;; +status) + if [ -z "$pduip" ]; then + exit 1 + fi + + if ping -w1 -c1 $pduip >/dev/null 2>&1; then + exit 0 + else + exit 1 + fi + ;; +getconfignames) + echo "hostlist pduip community" + exit 0 + ;; +getinfo-devid) + echo "rackpdu STONITH device" + exit 0 + ;; +getinfo-devname) + echo "rackpdu STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "APC Switched Rack PDU" + exit 0 + ;; +getinfo-devurl) + echo "http://www.apcc.com/products/family/index.cfm?id=30" + exit 0 + ;; +getinfo-xml) + cat << PDUXML +<parameters> + <parameter name="hostlist" unique="1" required="1"> + <content type="string" default="AUTO" /> + <shortdesc lang="en">Hostlist</shortdesc> + <longdesc lang="en"> +The list of hosts that the STONITH device controls (comma or space separated). +If you set value of this parameter to AUTO, list of hosts will be get from Rack PDU device. + </longdesc> + </parameter> + + <parameter name="pduip" unique="1" required="1"> + <content type="string" /> + <shortdesc lang="en">Name or IP address of Rack PDU device.</shortdesc> + <longdesc lang="en">Name or IP address of Rack PDU device.</longdesc> + </parameter> + + <parameter name="community" unique="1" required="1"> + <content type="string" default="private" /> + <shortdesc lang="en">Name of write community.</shortdesc> + <longdesc lang="en">Name of write community.</longdesc> + </parameter> + + <parameter name="oid" unique="1" required="0"> + <content type="string" /> + <shortdesc lang="en"> + The OID without the outlet number. + </shortdesc> + <longdesc lang="en"> +The SNMP OID for the PDU. minus the outlet number. +Try .1.3.6.1.4.1.318.1.1.12.3.3.1.1.4 (default value) +or use mib from ftp://ftp.apcc.com/apc/public/software/pnetmib/mib/ +Varies on different APC hardware and firmware. +Warning! No dot at the end of OID + </longdesc> + </parameter> + + <parameter name="names_oid" unique="1" required="0"> + <content type="string" /> + <shortdesc lang="en">The OID for getting names of outlets.</shortdesc> + <longdesc lang="en"> +The SNMP OID for getting names of outlets. +It is required to recognize outlet number by nodename. +Try ".1.3.6.1.4.1.318.1.1.12.3.3.1.1.2" (default value) +or use mib from ftp://ftp.apcc.com/apc/public/software/pnetmib/mib/ +Names of nodes must be equal names of outlets, in other way use outlet_config parameter. +If you set 'names_oid' parameter then parameter outlet_config must not be use. +Varies on different APC hardware and firmware. +Warning! No dot at the end of OID + </longdesc> + </parameter> + + <parameter name="outlet_config" unique="1" required="0"> + <content type="string" /> + <shortdesc lang="en">Configuration file. Other way to recognize outlet number by nodename.</shortdesc> + <longdesc lang="en"> +Configuration file. Other way to recognize outlet number by nodename. +Configuration file which contains +node_name=outlet_number +strings. + +Example: +server1=1 +server2=2 + +If you use outlet_config parameter then names_oid parameter can have any value and it is not uses. + </longdesc> + </parameter> + +</parameters> +PDUXML + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/riloe b/lib/plugins/stonith/external/riloe new file mode 100644 index 0000000..ce98847 --- /dev/null +++ b/lib/plugins/stonith/external/riloe @@ -0,0 +1,530 @@ +#!/usr/bin/env python +# +# Stonith module for RILOE Stonith device +# +# Copyright (c) 2004 Alain St-Denis <alain.st-denis@ec.gc.ca> +# +# Modified by Alan Robertson <alanr@unix.sh> for STONITH external compatibility. +# +# Extended and merged by Tijl Van den broeck <subspawn@gmail.com> +# with ilo-v2 script from Guy Coates +# +# Cleanup by Andrew Beekhof <abeekhof@suse.de> +# +# Rewritten by Dejan Muhamedagic <dejan@suse.de> +# Now, the plugin actually reads replies from iLO. +# +# Extended by Jochen Roeder <jochen.roeder@novell.com> +# to enable access via proxies +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys +import os +import socket +import subprocess +import xml.dom.minidom +import httplib +import time +import re + +def log_msg(level,msg): + subprocess.call("ha_log.sh %s '%s'" % (level,msg), shell=True) +def my_err(msg): + log_msg("err", msg) +def my_warn(msg): + log_msg("warn", msg) +def my_debug(msg): + log_msg("debug", msg) +def fatal(msg): + my_err(msg) + sys.exit(1) + +argv = sys.argv + +try: + cmd = argv[1] +except IndexError: + my_err("Not enough arguments") + sys.exit(1) + +legacy_RI_HOST = os.environ.get('RI_HOST', '') +legacy_RI_HOSTRI = os.environ.get('RI_HOSTRI', '') +legacy_RI_LOGIN = os.environ.get('RI_LOGIN', 'Administrator') +legacy_RI_PASSWORD = os.environ.get('RI_PASSWORD', '') + +reset_ok = os.environ.get('ilo_can_reset', '0') +ilo_protocol = os.environ.get('ilo_protocol', '1.2') +power_method = os.environ.get('ilo_powerdown_method', 'power') + +realhost = os.environ.get('hostlist', legacy_RI_HOST) +rihost = os.environ.get('ilo_hostname', legacy_RI_HOSTRI) +ilouser = os.environ.get('ilo_user', legacy_RI_LOGIN) +ilopass = os.environ.get('ilo_password', legacy_RI_PASSWORD) +iloproxyhost = os.environ.get('ilo_proxyhost', '') +try: + iloproxyport = int(os.environ.get('ilo_proxyport', 3128)) +except ValueError: + my_err("ilo_proxyport is not a number") + sys.exit(1) + +xmlinfo = '''<parameters> + <parameter name="hostlist" unique="1" required="1"> + <content type="string"/> + <shortdesc lang="en">ilo target hostname</shortdesc> + <longdesc lang="en"> + Contains the hostname that the ilo controls + </longdesc> + </parameter> +<parameter name="ilo_hostname" unique="1" required="1"> + <content type="string"/> + <shortdesc lang="en">ilo device hostname</shortdesc> + <longdesc lang="en"> + The hostname of the ilo device + </longdesc> + </parameter> +<parameter name="ilo_user" unique="0" required="1"> + <content type="string" default="Administrator"/> + <shortdesc lang="en">ilo user</shortdesc> + <longdesc lang="en"> + The user for connecting to the ilo device + </longdesc> + </parameter> +<parameter name="ilo_password" unique="0" required="1"> + <content type="string" default=""/> + <shortdesc lang="en">password</shortdesc> + <longdesc lang="en"> + The password for the ilo device user + </longdesc> + </parameter> +<parameter name="ilo_can_reset" unique="0" required="0"> + <content type="string" default="0"/> + <shortdesc lang="en">Device can reset</shortdesc> + <longdesc lang="en"> + Does the ILO device support RESET commands (hint: older ones cannot) + </longdesc> + </parameter> +<parameter name="ilo_protocol" unique="0" required="0"> + <content type="string" default="1.2"/> + <shortdesc lang="en">ILO Protocol</shortdesc> + <longdesc lang="en"> + Protocol version supported by the ILO device. + Known supported versions: 1.2, 2.0 + </longdesc> + </parameter> +<parameter name="ilo_powerdown_method" unique="0" required="0"> + <content type="string" default="power"/> + <shortdesc lang="en">Power down method</shortdesc> + <longdesc lang="en"> + The method to powerdown the host in question. + * button - Emulate holding down the power button + * power - Emulate turning off the machines power + + NB: A button request takes around 20 seconds. The power method + about half a minute. + </longdesc> + </parameter> +<parameter name="ilo_proxyhost" unique="0" required="0"> + <content type="string" default=""/> + <shortdesc lang="en">Proxy hostname</shortdesc> + <longdesc lang="en"> + proxy hostname if required to access ILO board + </longdesc> + </parameter> +<parameter name="ilo_proxyport" unique="0" required="0"> + <content type="string" default="3128"/> + <shortdesc lang="en">Proxy port</shortdesc> + <longdesc lang="en"> + proxy port if required to access ILO board + parameter will be ignored if proxy hostname is not set + </longdesc> + </parameter> + +</parameters>''' + +info = { + 'getinfo-devid': 'iLO2', + 'getinfo-devname': 'ilo2 ' + rihost, + 'getinfo-devdescr': 'HP/COMPAQ iLO2 STONITH device', + 'getinfo-devurl': 'http://www.hp.com/', + 'gethosts': realhost, + 'getinfo-xml': xmlinfo +} + +if cmd in info: + print info[cmd] + sys.exit(0) + +if cmd == 'getconfignames': + for arg in [ "hostlist", "ilo_hostname", "ilo_user", "ilo_password", "ilo_can_reset", "ilo_protocol", "ilo_powerdown_method", "ilo_proxyhost", "ilo_proxyport"]: + print arg + sys.exit(0) + +if not rihost: + fatal("ILO device hostname not specified") + +if not realhost: + fatal("Host controlled by this ILO device not specified") + +if not power_method in ("power","button"): + my_err('unknown power method %s, setting to "power"') + power_method = "power" + +# XML elements +E_RIBCL = "RIBCL" +E_LOGIN = "LOGIN" +E_SERVER_INFO = "SERVER_INFO" + +# power mgmt methods +E_RESET = "RESET_SERVER" # error if powered off +E_COLD_BOOT = "COLD_BOOT_SERVER" # error if powered off +E_WARM_BOOT = "WARM_BOOT_SERVER" # error if powered off +E_PRESS_BUTTON = "PRESS_PWR_BTN" +E_HOLD_BUTTON = "HOLD_PWR_BTN" + +# get/set status elements +E_SET_POWER = "SET_HOST_POWER" +E_GET_PSTATUS = "GET_HOST_POWER_STATUS" + +# whatever this means, but we have to use it to get good XML +E_LOCFG = "LOCFG" +LOCFG_VER = '2.21' + +# attributes +A_VERSION = "VERSION" # ilo_protocol +A_USER = "USER_LOGIN" +A_PWD = "PASSWORD" +A_MODE = "MODE" # info mode (read or write) +A_POWER_SW = "HOST_POWER" # "Y" or "N" +A_POWER_STATE = "HOST_POWER" # "ON" or "OFF" + +def new_power_req(tag, name = None, value = None): + ''' + Create a new RIBCL request (as XML). + ''' + my_debug("creating power request: %s,%s,%s"%(tag,name,value)) + doc = xml.dom.minidom.Document() + locfg = doc.createElement(E_LOCFG) + locfg.setAttribute(A_VERSION,LOCFG_VER) + ribcl = doc.createElement(E_RIBCL) + ribcl.setAttribute(A_VERSION,ilo_protocol) + login = doc.createElement(E_LOGIN) + login.setAttribute(A_USER,ilouser) + login.setAttribute(A_PWD,ilopass) + serv_info = doc.createElement(E_SERVER_INFO) + # read or write, it doesn't really matter, i.e. even if we + # say "write" that doesn't mean we can't read + serv_info.setAttribute(A_MODE,"write") + doc.appendChild(locfg) + locfg.appendChild(ribcl) + ribcl.appendChild(login) + login.appendChild(serv_info) + el_node = doc.createElement(tag) + if name: + el_node.setAttribute(name,value) + serv_info.appendChild(el_node) + s = doc.toprettyxml() + doc.unlink() + # work around an iLO bug: last line containing "</LOCFG>" + # produces a syntax error + lines = s.split('\n') + return '\n'.join(lines[:-2]) + +E_RESPONSE = "RESPONSE" +E_HOST_POWER = "GET_HOST_POWER" +A_STATUS = "STATUS" +# documentation mentions both; better safe than sorry +A_MSG = "MSG" +A_MSG2 = "MESSAGE" + +def is_element(xmlnode): + return xmlnode.nodeType == xmlnode.ELEMENT_NODE + +def read_resp(node): + ''' + Check if the RESPONSE XML is OK. + ''' + msg = "" + str_status = "" + for attr in node.attributes.keys(): + if attr == A_STATUS: + str_status = node.getAttribute(attr) + elif attr == A_MSG: + msg = node.getAttribute(attr) + elif attr == A_MSG2: + msg = node.getAttribute(attr) + else: + my_warn("unexpected attribute %s in %s" % (attr,E_RESPONSE)) + if not str_status: + my_err("no status in response") + return -1 + try: + status = int(str_status,16) + except ValueError: + my_err("unexpected status %s in response" % str_status) + return -1 + if status != 0: + my_err("%s (rc: %s)"%(msg,str_status)) + return -1 + return 0 + +def read_power(node): + ''' + Read the power from the XML node. Set the global power + variable correspondingly. + ''' + global power + for attr in node.attributes.keys(): + if attr == A_POWER_STATE: + power_state = node.getAttribute(attr).upper() + else: + my_warn("unexpected attribute %s in %s" % (attr,node.tagName)) + if not power_state: + my_err("no %s attribute in %s" % (A_POWER_STATE,node.tagName)) + return -1 + if power_state not in ("ON","OFF"): + my_err("unexpected value for %s: %s" % (A_POWER_STATE,power_state)) + return -1 + power = (power_state == "ON") + my_debug("Host has power: %s"%power) + return 0 + +el_parsers = { + E_RESPONSE:read_resp, + E_HOST_POWER:read_power +} +def proc_resp(doc): + ''' + Process one iLO reply. Real work is done in el_parsers. + ''' + ribcl = doc.childNodes[0] + if not is_element(ribcl) or ribcl.tagName != E_RIBCL: + my_err("unexpected top element in response") + return -1 + for child in ribcl.childNodes: + if not is_element(child): + continue + if child.tagName in el_parsers: + rc = el_parsers[child.tagName](child) + if rc != 0: + return -1 + else: + my_warn("unexpected element in response: %s" % child.toxml()) + return 0 + +def open_ilo(host): + # open https connection + try: + if iloproxyhost != "" and iloproxyport != 0: + proxy=socket.socket(socket.AF_INET,socket.SOCK_STREAM) + proxy.connect((iloproxyhost, iloproxyport)) + proxy_connect='CONNECT %s:%s HTTP/1.1\r\n'%(host,443) + user_agent='User-Agent: python\r\n' + proxy_pieces=proxy_connect+user_agent+'\r\n' + proxy.sendall(proxy_pieces) + response=proxy.recv(8192) + status=response.split()[1] + if status!=str(200): + fatal("Error status=: %s" %(response)) + import ssl + sock = ssl.wrap_socket(proxy) + h=httplib.HTTPConnection('localhost') + h.sock=sock + return h + else: + return httplib.HTTPSConnection(host) + except socket.gaierror, msg: + fatal("%s: %s" %(msg,host)) + except socket.sslerror, msg: + fatal("%s for %s" %(msg,host)) + except socket.error, msg: + fatal("%s while talking to %s" %(msg,host)) + except ImportError, msg: + fatal("ssl support missing (%s)" %msg) + +def send_request(req,proc_f): + ''' + 1. After every request, the iLO closes the connection. + 2. For every request, there are multiple replies. Each reply + is an XML document. Most of replies are just a kind of + (verbose) XML "OK". + ''' + t_begin = time.time() + c = open_ilo(rihost) + try: + c.send(req+'\r\n') + except socket.error, msg: + fatal("%s, while talking to %s" %(msg,rihost)) + t_end = time.time() + my_debug("request sent in %0.2f s" % ((t_end-t_begin))) + + t_begin = time.time() + result = [] + while True: + try: + reply = c.sock.recv(1024) + if not reply: + break + result.append(reply) + except socket.error, msg: + if msg[0] == 6: # connection closed + break + my_err("%s, while talking to %s" %(msg,rihost)) + return -1 + c.close() + t_end = time.time() + + if not result: + fatal("no response from %s within %0.2f s"%(rihost,(t_end-t_begin))) + for reply in result: + # work around the iLO bug, i.e. element RIBCL closed twice + if re.search("</RIBCL", reply) and re.search("<RIBCL.*/>", reply): + reply = re.sub("<(RIBCL.*)/>", r"<\1>", reply) + try: + doc = xml.dom.minidom.parseString(reply) + except xml.parsers.expat.ExpatError,msg: + fatal("malformed response: %s\n%s"%(msg,reply)) + rc = proc_f(doc) + doc.unlink() + if rc != 0: + break + my_debug("iLO processed request (rc=%d) in %0.2f s" % (rc,(t_end-t_begin))) + return rc + +def manage_power(cmd): + ''' + Before trying to send a request we have to check the power + state. + ''' + rc = 0 + req = '' + # it won't do to turn it on if it's already on! + if cmd == "on" and not power: + req = new_power_req(E_SET_POWER,A_POWER_SW,"Y") + # also to turn it off if it's already off + elif cmd == "off" and power: + req = new_power_req(E_SET_POWER,A_POWER_SW,"N") + elif cmd == "cold_boot" and power: + req = new_power_req(E_COLD_BOOT) + elif cmd == "warm_boot" and power: + req = new_power_req(E_WARM_BOOT) + elif cmd == "reset": + if power: + req = new_power_req(E_RESET) + # reset doesn't work if the host's off + else: + req = new_power_req(E_SET_POWER,A_POWER_SW,"Y") + if req: + rc = send_request(req,proc_resp) + return rc +def power_on(): + ''' + Update the power variable without checking the power state. + The iLO is slow at times to report the actual power state, so + we assume that it changed if the request succeeded. + ''' + rc = manage_power("on") + if rc == 0: + global power + power = True + return rc +def power_off(): + rc = manage_power("off") + if rc == 0: + global power + power = False + return rc +def cold_boot(): + rc = manage_power("cold_boot") + return rc +def warm_boot(): + rc = manage_power("warm_boot") + return rc +def reset(): + rc = manage_power("reset") + if rc == 0: + global power + power = True + return rc +def hold_button(): + ''' + Hold the power button. Got this error message when tried + without the TOGGLE attribute: + Command without TOGGLE="Yes" attribute is ignored + when host power is off. (rc: 0x0054) + Didn't find any documentation about TOGGLE. + ''' + if power: + req = new_power_req(E_HOLD_BUTTON) + else: + req = new_power_req(E_HOLD_BUTTON,"TOGGLE","Yes") + rc = send_request(req,proc_resp) + return rc +def read_power_state(): + req = new_power_req(E_GET_PSTATUS) + rc = send_request(req,proc_resp) + return rc + +def reset_power(): + ''' + Three methods to reset: + - hold power button + - reset (only if host has power and user said that reset is ok) + - power off/on + ''' + do_power_on = False + if power_method == 'button': + rc = hold_button() + elif reset_ok != '0': + if power: + return reset() + else: + return power_on() + else: + do_power_on = True + rc = power_off() + if rc == 0: + rc = read_power_state() + if do_power_on: + while rc == 0 and power: # wait for the power state to go off + time.sleep(5) + rc = read_power_state() + if rc == 0 and do_power_on and not power: + rc = power_on() + return rc + +# track state of host power +power = -1 + +todo = { +'reset':reset_power, +'on':power_on, +'off':power_off, +'cold':cold_boot, +'warm':warm_boot, +'status':lambda: 0 # just return 0, we already read the state +} + +rc = read_power_state() +if rc == 0: + if cmd in todo: + rc = todo[cmd]() + else: + fatal('Invalid command: %s' % cmd) +if rc != 0: + fatal("request failed") +sys.exit(rc) + +# vi:ts=4:sw=4:et: diff --git a/lib/plugins/stonith/external/ssh.in b/lib/plugins/stonith/external/ssh.in new file mode 100644 index 0000000..2a8eb73 --- /dev/null +++ b/lib/plugins/stonith/external/ssh.in @@ -0,0 +1,176 @@ +#!/bin/sh +# +# External STONITH module for ssh. +# +# Copyright (c) 2004 SUSE LINUX AG - Lars Marowsky-Bree <lmb@suse.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +SSH_COMMAND="@SSH@ -q -x -o PasswordAuthentication=no -o StrictHostKeyChecking=no -n -l root" +#SSH_COMMAND="@SSH@ -q -x -n -l root" + +REBOOT_COMMAND="echo 'sleep 2; @REBOOT@ @REBOOT_OPTIONS@' | SHELL=/bin/sh at now >/dev/null 2>&1" + +# Warning: If you select this poweroff command, it'll physically +# power-off the machine, and quite a number of systems won't be remotely +# revivable. +# TODO: Probably should touch a file on the server instead to just +# prevent heartbeat et al from being started after the reboot. +# POWEROFF_COMMAND="echo 'sleep 2; /sbin/poweroff -nf' | SHELL=/bin/sh at now >/dev/null 2>&1" +POWEROFF_COMMAND="echo 'sleep 2; @REBOOT@ @REBOOT_OPTIONS@' | SHELL=/bin/sh at now >/dev/null 2>&1" + +# Rewrite the hostlist to accept "," as a delimeter for hostnames too. +hostlist=`echo $hostlist | tr ',' ' '` + +is_host_up() { + for j in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + do + if + ping -w1 -c1 "$1" >/dev/null 2>&1 + then + sleep 1 + else + return 1 + fi + done + return 0 +} + + +case $1 in +gethosts) + for h in $hostlist ; do + echo $h + done + exit 0 + ;; +on) + # Can't really be implemented because ssh cannot power on a system + # when it is powered off. + exit 1 + ;; +off) + # Shouldn't really be implemented because if ssh cannot power on a + # system, it shouldn't be allowed to power it off. + exit 1 + ;; +reset) + h_target=`echo $2 | tr A-Z a-z` + for h in $hostlist + do + h=`echo $h | tr A-Z a-z` + [ "$h" != "$h_target" ] && + continue + if + case ${livedangerously} in + [Yy]*) is_host_up $h;; + *) true;; + esac + then + $SSH_COMMAND "$2" "$REBOOT_COMMAND" + # Good thing this is only for testing... + if + is_host_up $h + then + exit 1 + else + exit 0 + fi + else + # well... Let's call it successful, after all this is only for testing... + exit 0 + fi + done + exit 1 + ;; +status) + if + [ -z "$hostlist" ] + then + exit 1 + fi + for h in $hostlist + do + if + ping -w1 -c1 "$h" 2>&1 | grep "unknown host" + then + exit 1 + fi + done + exit 0 + ;; +getconfignames) + echo "hostlist" + exit 0 + ;; +getinfo-devid) + echo "ssh STONITH device" + exit 0 + ;; +getinfo-devname) + echo "ssh STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "ssh-based host reset" + echo "Fine for testing, but not suitable for production!" + echo "Only reboot action supported, no poweroff, and, surprisingly enough, no poweron." + exit 0 + ;; +getinfo-devurl) + echo "http://openssh.org" + exit 0 + ;; +getinfo-xml) + cat << SSHXML +<parameters> +<parameter name="hostlist" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostlist +</shortdesc> +<longdesc lang="en"> +The list of hosts that the STONITH device controls +</longdesc> +</parameter> + +<parameter name="livedangerously" unique="0" required="0"> +<content type="enum" /> +<shortdesc lang="en"> +Live Dangerously!! +</shortdesc> +<longdesc lang="en"> +Set to "yes" if you want to risk your system's integrity. +Of course, since this plugin isn't for production, using it +in production at all is a bad idea. On the other hand, +setting this parameter to yes makes it an even worse idea. +Viva la Vida Loca! +</longdesc> +</parameter> + +</parameters> +SSHXML + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/vcenter b/lib/plugins/stonith/external/vcenter new file mode 100755 index 0000000..71a6302 --- /dev/null +++ b/lib/plugins/stonith/external/vcenter @@ -0,0 +1,280 @@ +#!/usr/bin/env perl +# +# External STONITH module for VMWare vCenter/ESX +# +# Author: Nhan Ngo Dinh +# License: GNU General Public License (GPL) +# + +require 5.010; + +use strict; +use warnings; + +sub dielog { + my $msg = "["; + $msg .= "$ARGV[0]" if defined($ARGV[0]); + $msg .= " $ARGV[1]" if defined($ARGV[1]); + $msg .= "]"; + ( $_ ) = @_; + $msg .= " $_"; + system("ha_log.sh", "err", "$msg"); + die(); +} + +# Define command groups +my @configCommands = qw{getconfignames getinfo-devid getinfo-devname getinfo-devdescr getinfo-devurl getinfo-xml}; +my @actionCommands = qw{reset on off}; +my @netCommands = (@actionCommands, qw{status gethosts listvms}); + +# Process command line arguments +my $command = $ARGV[0] || dielog("No command specified\n"); + +# Command belongs to the group of commands that do not require any connection to VMware vCenter +if ($command ~~ @configCommands) { + if ($command eq "getconfignames") { + print "VI_SERVER\nVI_PORTNUMBER\nVI_PROTOCOL\nVI_SERVICEPATH\nVI_CREDSTORE\nHOSTLIST\nRESETPOWERON\n"; + } + elsif ($command eq "getinfo-devid") { + print "VMware vCenter STONITH device\n"; + } + elsif ($command eq "getinfo-devname") { + print "VMware vCenter STONITH device\n"; + } + elsif ($command eq "getinfo-devdescr") { + print "VMWare vCenter STONITH device\n"; + } + elsif ($command eq "getinfo-devurl") { + print "http://www.vmware.com/\n"; + } + elsif ($command eq "getinfo-xml") { + print q{<parameters> +<parameter name="HOSTLIST" required="1"> +<content type="string"/> +<shortdesc lang="en">List of hosts and virtual machines (required)</shortdesc> +<longdesc lang="en"> +The list of hosts that the VMware vCenter STONITH device controls. +Syntax is: + hostname1[=VirtualMachineName1];hostname2[=VirtualMachineName2] + +NOTE: omit =VirtualMachineName if hostname and virtual machine names are identical + +Example: + cluster1=VMCL1;cluster2=VMCL2 +</longdesc> +</parameter> +<parameter name="VI_SERVER"> +<content type="string" default="localhost"/> +<shortdesc lang="en">VMware vCenter address</shortdesc> +<longdesc lang="en"> +The VMware vCenter address +</longdesc> +</parameter> +<parameter name="VI_PROTOCOL"> +<content type="string" default="https"/> +<shortdesc lang="en">VMware vCenter protocol</shortdesc> +<longdesc lang="en"> +The VMware vCenter protocol +</longdesc> +</parameter> +<parameter name="VI_PORTNUMBER"> +<content type="string" default="443"/> +<shortdesc lang="en">VMware vCenter port number</shortdesc> +<longdesc lang="en"> +The VMware vCenter port number +</longdesc> +</parameter> +<parameter name="VI_SERVICEPATH"> +<content type="string" default="/sdk"/> +<shortdesc lang="en">VMware vCenter service path</shortdesc> +<longdesc lang="en"> +The VMware vCenter services path +</longdesc> +</parameter> +<parameter name="VI_CREDSTORE" required="1"> +<content type="string"/> +<shortdesc lang="en">VMware vCenter credentials store file</shortdesc> +<longdesc lang="en"> +VMware vCenter credentials store file +</longdesc> +</parameter> +<parameter name="RESETPOWERON"> +<content type="string" default="1"/> +<shortdesc lang="en">PowerOnVM on reset</shortdesc> +<longdesc lang="en"> +Enable/disable a PowerOnVM on reset when the target virtual machine is off +Allowed values: 0, 1 +</longdesc> +</parameter> +<parameter name="PERL_LWP_SSL_VERIFY_HOSTNAME"> +<content type="string"/> +<shortdesc lang="en">Enable or disable SSL hostname verification</shortdesc> +<longdesc lang="en"> +To disable SSL hostname verification set this option to 0. +To enable hostname verification, set this option to 1. +This option is actually part of the LWP Perl library. +See LWP(3pm) for more information. +</longdesc> +</parameter> +</parameters>} . "\n"; + } + else { dielog("Invalid command specified: $command\n"); } +} + +# Command belongs to the group of commands that require connecting to VMware vCenter +elsif ($command ~~ @netCommands) { + + eval { require VMware::VIRuntime; } + or dielog("Missing perl module VMware::VIRuntime. Download and install 'VMware Infrastructure (VI) Perl Toolkit', available at http://www.vmware.com/support/developer/viperltoolkit/ \n"); + + # A valid VI_CREDSTORE is required to avoid interactive prompt + ( exists $ENV{'VI_CREDSTORE'} ) || dielog("VI_CREDSTORE not specified\n"); + + # HOSTLIST is mandatory + exists $ENV{'HOSTLIST'} || dielog("HOSTLIST not specified\n"); + + # Parse HOSTLIST to %host_to_vm and %vm_to_host + my @hostlist = split(';', $ENV{'HOSTLIST'}); + my %host_to_vm = (); + my %vm_to_host = (); + foreach my $host (@hostlist) { + my @config = split(/=/, $host); + my $key = $config[0]; my $value = $config[1]; + if (!defined($value)) { $value = $config[0]; } + $host_to_vm{$key} = $value; + $vm_to_host{(lc $value)} = $key; + } + + eval { + # VI API: reads options from the environment variables into appropriate data structures for validation. + Opts::parse(); + # VI API: ensures that input values from environment variable are complete, consistent and valid. + Opts::validate(); + # VI API: establishes a session with the VirtualCenter Management Server or ESX Server Web service + Util::connect(); + }; + if ($@) { + # This is just a placeholder for any error handling procedure + dielog($@); + } + + # Command belongs to the group of commands that performs actions on Virtual Machines + if ($command ~~ @actionCommands) { + + my $targetHost = $ARGV[1] || dielog("No target specified\n"); + + # Require that specified target host exists in the specified HOSTLIST + if (exists $host_to_vm{$targetHost}) { + + my $vm; + my $esx; + eval { + # VI API: searches the inventory tree for a VirtualMachine managed entity whose name matches + # the name of the virtual machine assigned to the target host in HOSTLIST + $vm = Vim::find_entity_view(view_type => "VirtualMachine", filter => { name => qr/^\Q$host_to_vm{$targetHost}\E/i }); + if (!defined $vm) { + dielog("Machine $targetHost was not found"); + } + + # VI API: retrieves the properties of the managed object reference runtime.host of the VirtualMachine + # managed entity obtained by the previous command + # NOTE: This is essentially a workaround to vSphere Perl SDK + # to allow pointing to the right HostSystem. This is probably + # done by changing the current HostSystem in the Web Service + # session context. WARNING: Do not use the same session for any + # other concurrent operation. + $esx = Vim::get_view(mo_ref => $vm->{"runtime"}{"host"})->name; + }; + if ($@) { + if (ref($@) eq "SoapFault") { dielog("$@->detail\n"); } + dielog($@); + } + + my $powerState = $vm->get_property('runtime.powerState')->val; + if ($powerState eq "suspended") { + # This implementation assumes that suspending a cluster node can cause + # severe failures on shared resources, thus any failover operation should + # be blocked. + dielog("Machine $esx:$vm->{'name'} is in a suspended state\n"); + } + + eval { + if ($command eq "reset") { + if ($powerState eq "poweredOn") { + $vm->ResetVM(); + system("ha_log.sh", "info", "Machine $esx:$vm->{'name'} has been reset"); + } else { + system("ha_log.sh", "warn", "Tried to ResetVM $esx:$vm->{'name'} that was $powerState"); + # Start a virtual machine on reset only if explicitly allowed by RESETPOWERON + if ($powerState eq "poweredOff" && (! exists $ENV{'RESETPOWERON'} || $ENV{'RESETPOWERON'} ne 0)) { + $vm->PowerOnVM(); + system("ha_log.sh", "info", "Machine $esx:$vm->{'name'} has been powered on"); + } else { + dielog("Could not complete $esx:$vm->{'name'} power cycle"); + } + } + } + elsif ($command eq "off") { + if ($powerState eq "poweredOn") { + $vm->PowerOffVM(); + system("ha_log.sh", "info", "Machine $esx:$vm->{'name'} has been powered off"); + } else { + system("ha_log.sh", "warn", "Tried to PowerOffVM $esx:$vm->{'name'} that was $powerState"); + + } + } + elsif ($command eq "on") { + if ($powerState eq "poweredOff") { + $vm->PowerOnVM(); + system("ha_log.sh", "info", "Machine $esx:$vm->{'name'} has been powered on"); + } else { + system("ha_log.sh", "warn", "Tried to PowerOnVM $esx:$vm->{'name'} that was $powerState"); + } + } + else { dielog("Invalid command specified: $command\n"); } + }; + if ($@) { + if (ref($@) eq "SoapFault") { dielog("$@->detail\n"); } + dielog($@); + } + + } else { dielog("Invalid target specified\n"); } + } else { + # Command belongs to the group of commands that lookup the status of VMware vCenter and/or virtual machines + if ($command eq "status") { + # we already connect to the vcenter, no need to do + # anything else in status + ; + } + elsif ($command eq "gethosts") { + foreach my $key (keys(%host_to_vm)) { + print "$key \n"; + } + } + elsif ($command eq "listvms") { + eval { + # VI API: Searches the inventory tree for all VirtualMachine managed objects + my $vms = Vim::find_entity_views(view_type => "VirtualMachine"); + if (defined $vms) { + printf(STDERR "%-50s %-20s\n", "VM Name", "Power state"); + print STDERR "-" x 70 . "\n"; + foreach my $vm (@$vms) { + my $powerState = $vm->get_property('runtime.powerState')->val; + printf("%-50s %-20s\n", $vm->{name}, $powerState); + } + } + }; + } + else { dielog("Invalid command specified: $command\n"); } + } + eval { + Util::disconnect(); + }; + if ($@) { + # This is just a placeholder for any error handling procedure + dielog($@); + } +} +else { dielog("Invalid command specified: $command\n"); } + +exit(0); diff --git a/lib/plugins/stonith/external/vmware b/lib/plugins/stonith/external/vmware new file mode 100644 index 0000000..55966ba --- /dev/null +++ b/lib/plugins/stonith/external/vmware @@ -0,0 +1,216 @@ +#!/usr/bin/perl +# External STONITH module for VMWare Server Guests +# +# Copyright (c) 2004 SUSE LINUX AG - Andrew Beekhof <abeekhof@suse.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of version 2 of the GNU General Public License as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it would be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# +# Further, this software is distributed without any warranty that it is +# free of the rightful claim of any third person regarding infringement +# or the like. Any license provided herein, whether implied or +# otherwise, applies only to this software file. Patent licenses, if +# any, provided herein do not apply to combinations of this program with +# other software, or any other product whatsoever. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA. +# + +sub supply_default +{ + my $name = $_[0]; + my $value = $_[1]; + + if ( defined $ENV{$name} ) { + #print "Set: $name=$ENV{$name}\n"; + } else { + $ENV{$name} = $value; + #print "Default: $name=$ENV{$name}\n"; + } +} + +sub vmware_command +{ + my $config = $_[0]; + my $action = $_[1]; + my @lines; + + my $device = $ENV{'device_host'}; + + if ( $device =~ /localhost/ ) { + @lines = readpipe "vmware-cmd $config $action"; + + } else { + @lines = readpipe "ssh $device \"vmware-cmd \\\"$config\\\" $action\""; + } + + #print @lines; + return @lines; +} + +sub is_host_active +{ + my $config = config_for_host($_[0]); + my @lines = vmware_command($config, "getstate"); + foreach $line (@lines) { + if ( $line =~ /getstate.* = on/ ) { + return 1; + } + } + return 0; +} + +sub supported_hosts +{ + my $line; + my @lines; + + if ( defined $ENV{'host_map'} ) { + @lines = split(/;/, $ENV{'host_map'}); + foreach $line (@lines){ + @config = split(/=/, $line); + $host = $config[0]; + if ( is_host_active($host) == 1 ) { + print "$host\n"; + } + } + + } else { + @lines = vmware_command("-l"); + foreach $line (@lines){ + my @elements = split(/\//, $line); + $host = $elements[$#elements-1]; + if ( is_host_active($host) == 1 ) { + print "$host\n"; + } + } + } +} + +sub config_for_host +{ + my $line; + my @lines; + my $target = $_[0]; + + if ( defined $ENV{'host_map'} ) { + @lines = split(/;/, $ENV{'host_map'}); + foreach $line (@lines){ + if ( $line =~ /^$target=/ ) { + @config = split(/=/, $line); + return $config[1]; + } + } + + } else { + @lines = vmware_command("-l"); + + foreach $line (@lines){ + if ( $line =~ /\/$target\// ) { + chop($line); + return $line; + } + } + } +} + +$command = $ARGV[0]; +if ( defined $ARGV[1] ) { + $targetHost = $ARGV[1]; +} + +supply_default("device_host", "localhost"); + +if ( $command =~ /^gethosts$/ ) { + supported_hosts; + +} elsif ( $command =~ /^getconfignames$/ ) { + print "device_host\n"; + +} elsif ( $command =~ /^getinfo-devid$/ ) { + print "VMware Server STONITH device\n"; +} elsif ( $command =~ /^getinfo-devname$/ ) { + print "VMware Server STONITH device\n"; +} elsif ( $command =~ /^getinfo-devdescr$/ ) { + print "VMware Server STONITH device\n"; +} elsif ( $command =~ /^getinfo-devurl$/ ) { + print "http://www.vmware.com/"; + +} elsif ( $command =~ /^on$/ ) { + $config = config_for_host($targetHost); + my @lines = vmware_command($config, "start hard"); + print @lines; + +} elsif ( $command =~ /^off$/ ) { + $config = config_for_host($targetHost); + my @lines = vmware_command($config, "stop hard"); + print @lines; + +} elsif ( $command =~ /^reset$/ ) { + $config = config_for_host($targetHost); + my @lines = vmware_command($config, "reset hard"); + print @lines; + +} elsif ( $command =~ /^status$/ ) { + my $rc = 7; + my $device = $ENV{'device_host'}; + if ( $device =~ /localhost/ ) { + $rc = 0; + # TODO: Check for the vmware process + print "Local version: always running\n"; + + } else { + print "Remote version: running ping\n"; + @lines = readpipe "ping -c1 $device"; + print @lines; + + foreach $line ( @lines ) { + if ( $line =~ /0% packet loss/ ) { + $rc = 0; + last; + } + } + } + exit($rc); + +} elsif ( $command =~ /^getinfo-xml$/ ) { + $metadata = <<END; +<parameters> +<parameter name="host_map" unique="0" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Host Map +</shortdesc> +<longdesc lang="en"> +A mapping of hostnames to config paths supported by this device. +Eg. host1=/config/path/1;host2=/config/path/2;host3=/config/path/3; +</longdesc> +</parameter> +<parameter name="device_host" unique="0" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Device Host +</shortdesc> +<longdesc lang="en"> +The machine _hosting_ the virtual machines +</longdesc> +</parameter> +</parameters> +END + +print $metadata; + +} else { + print "Command $command: not supported\n"; + exit(3); # Not implemented +} + + +exit(0); diff --git a/lib/plugins/stonith/external/xen0 b/lib/plugins/stonith/external/xen0 new file mode 100644 index 0000000..ef1ee40 --- /dev/null +++ b/lib/plugins/stonith/external/xen0 @@ -0,0 +1,253 @@ +#!/bin/sh +# +# External STONITH module for Xen Dom0 through ssh. +# +# Description: Uses Xen Dom0 Domain as a STONITH device +# to control DomUs. +# +# +# Author: Serge Dubrouski (sergeyfd@gmail.com) +# Inspired by Lars Marowsky-Bree's external/ssh agent. +# +# Copyright 2007 Serge Dubrouski <sergeyfd@gmail.com> +# License: GNU General Public License (GPL) +# + +STOP_COMMAND="xm destroy" +START_COMMAND="xm create" +DUMP_COMMAND="xm dump-core" +DEFAULT_XEN_DIR="/etc/xen" +SSH_COMMAND="/usr/bin/ssh -q -x -n" + +# Rewrite the hostlist to accept "," as a delimeter for hostnames too. +hostlist=`echo $hostlist | tr ',' ' '` + +CheckIfDead() { + for j in 1 2 3 4 5 + do + if ! ping -w1 -c1 "$1" >/dev/null 2>&1 + then + return 0 + fi + sleep 1 + done + + return 1 +} + +CheckHostList() { + if [ "x" = "x$hostlist" ] + then + ha_log.sh err "hostlist isn't set" + exit 1 + fi +} + +CheckDom0() { + if [ "x" = "x$dom0" ] + then + ha_log.sh err "dom0 isn't set" + exit 1 + fi +} + +RunCommand() { + CheckHostList + CheckDom0 + + for h in $hostlist + do + CIFS=$IFS + IFS=: + read node cfg << -! +$h +-! + IFS=$CIFS + + if [ "x" = "x$node" ] + then + ha_log.sh err "Syntax error in host list" + exit 1 + fi + + if [ "x" = "x$cfg" ] + then + cfg="${DEFAULT_XEN_DIR}/${node}.cfg" + fi + + if [ "$node" != "$1" ] + then + continue + fi + + case $2 in + stop) + kill_node=`$SSH_COMMAND $dom0 "grep ^[[:space:]]*name $cfg" | cut -f 2 -d '=' | sed -e 's,",,g'` + if [ "x" = "x$kill_node" ] + then + ha_log.sh err "Couldn't find a node name to stop" + exit 1 + fi + + if [ "x$run_dump" != "x" ] + then + #Need to run core dump + if [ "x$dump_dir" != "x" ] + then + #Dump with the specified core file + TIMESTAMP=`date +%Y-%m%d-%H%M.%S` + DOMAINNAME=`printf "%s" $kill_node` + COREFILE=$dump_dir/$TIMESTAMP-$DOMAINNAME.core + $SSH_COMMAND $dom0 "(mkdir -p $dump_dir; $DUMP_COMMAND $kill_node $COREFILE) >/dev/null 2>&1" + else + $SSH_COMMAND $dom0 "$DUMP_COMMAND $kill_node >/dev/null 2>&1" + fi + fi + $SSH_COMMAND $dom0 "(sleep 2; $STOP_COMMAND $kill_node) >/dev/null 2>&1 &" + break;; + start) + $SSH_COMMAND $dom0 "(sleep 2; $START_COMMAND $cfg) >/dev/null 2>&1 &" + break;; + esac + exit 0 + done +} + + +# Main code + +case $1 in +gethosts) + CheckHostList + + for h in $hostlist ; do + CIFS=$IFS + IFS=: + read node cfg << -! +$h +-! + IFS=$CIFS + + echo $node + done + exit 0 + ;; +on) + RunCommand $2 start + exit $? + ;; +off) + if RunCommand $2 stop + then + if CheckIfDead $2 + then + exit 0 + fi + fi + + exit 1 + ;; +reset) + RunCommand $2 stop + + if CheckIfDead $2 + then + RunCommand $2 start + exit 0 + fi + + exit 1 + ;; +status) + CheckHostList + + for h in $hostlist + do + CIFS=$IFS + IFS=: + read node cfg << -! +$h +-! + IFS=$CIFS + + echo $node + if ping -w1 -c1 "$node" 2>&1 | grep "unknown host" + then + exit 1 + fi + done + exit 0 + ;; +getconfignames) + echo "hostlist dom0" + exit 0 + ;; +getinfo-devid) + echo "xen0 STONITH device" + exit 0 + ;; +getinfo-devname) + echo "xen0 STONITH external device" + exit 0 + ;; +getinfo-devdescr) + echo "ssh-based host reset for Xen DomU trough Dom0" + echo "Fine for testing, but not really suitable for production!" + exit 0 + ;; +getinfo-devurl) + echo "http://openssh.org http://www.xensource.com/ http://linux-ha.org/wiki" + exit 0 + ;; +getinfo-xml) + cat << SSHXML +<parameters> +<parameter name="hostlist" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostlist +</shortdesc> +<longdesc lang="en"> +The list of controlled nodes in a format node[:config_file]. +For example: "node1:/opt/xen/node1.cfg node2" +If config file isn't set it defaults to /etc/xen/{node_name}.cfg +</longdesc> +</parameter> +<parameter name="dom0" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Dom0 +</shortdesc> +<longdesc lang="en"> +Name of the Dom0 Xen node. Root user shall be able to ssh to that node. +</longdesc> +</parameter> +<parameter name="run_dump" unique="0" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Run dump-core +</shortdesc> +<longdesc lang="en"> +If set plugin will call "xm dump-core" before killing DomU +</longdesc> +</parameter> +<parameter name="dump_dir" unique="1" required="0"> +<content type="string" /> +<shortdesc lang="en"> +Run dump-core with the specified directory +</shortdesc> +<longdesc lang="en"> +This parameter can indicate the dump destination. +Should be set as a full path format, ex.) "/var/log/dump" +The above example would dump the core, like; +/var/log/dump/2009-0316-1403.37-domU.core +</longdesc> +</parameter> +</parameters> +SSHXML + exit 0 + ;; +*) + exit 1 + ;; +esac diff --git a/lib/plugins/stonith/external/xen0-ha-dom0-stonith-helper b/lib/plugins/stonith/external/xen0-ha-dom0-stonith-helper new file mode 100755 index 0000000..b313f8b --- /dev/null +++ b/lib/plugins/stonith/external/xen0-ha-dom0-stonith-helper @@ -0,0 +1,72 @@ +#!/bin/bash +# Author: Lars Marowsky-Bree +# +# Copyright 2008 Lars Marowsky-Bree +# License: GNU General Public License (GPL) + +# This is not an external/stonith plugin by itself, but instead a helper +# script which gets installed in Dom0. + +# TODO: +# - Error handling +# - How to handle if the DomU resource doesn't exist? +# - Does this truly work with split-brain? +# - Is the handling of non-existent resources adequate? +# ... +# Basically: more testing. This is proof-of-concept and works, but deserves +# validation. + +CMD="$1" +DOMU="$2" +TIMEOUT="$3" + +# Make sure the timeout is an integer: +if [ "0$TIMEOUT" -eq 0 ]; then + TIMEOUT=300 +fi + +SetTargetRole() { + local new_role="$1" + crm_resource -r $DOMU --meta -p target_role -v $new_role + + local timeout="$TIMEOUT" + + # We only need to wait for "stopped". + if [ "$new_role" != "stopped" ]; then + return 0 + fi + + while [ $timeout -gt 0 ]; do + local rc + crm_resource -W -r $DOMU 2>&1 | grep -q "is NOT running" + rc=$? + if [ $rc -eq 0 ]; then + return 0 + fi + timeout=$[timeout-1]; + sleep 1 + done + return 1 +} + + +case $CMD in +on) SetTargetRole started + exit $? + ;; +off) SetTargetRole stopped + exit $? + ;; +reset) SetTargetRole stopped || exit 1 + SetTargetRole started + exit $? + ;; +status) exit 0 + ;; +*) ha_log.sh err "Called with unknown command: $CMD" + exit 1 + ;; +esac + +exit 1 + diff --git a/lib/plugins/stonith/external/xen0-ha.in b/lib/plugins/stonith/external/xen0-ha.in new file mode 100755 index 0000000..cb42cbc --- /dev/null +++ b/lib/plugins/stonith/external/xen0-ha.in @@ -0,0 +1,96 @@ +#!/bin/bash +# +# This STONITH script integrates a cluster running within DomUs +# with the CRM/Pacemaker cluster running in Dom0. +# +# Author: Lars Marowsky-Bree +# Copyright: 2008 Lars Marowsky-Bree +# License: GNU General Public License (GPL) +# + +SSH_COMMAND="@SSH@ -q -x -n" +HVM_HELPER="@stonith_plugindir@/xen0-ha-dom0-stonith-helper" + +# Rewrite the hostlist to accept "," as a delimeter for hostnames too. +hostlist=`echo $hostlist | tr ',' ' '` + +# Runs a command on the host, waiting for it to return +RunHVMCommand() { + $SSH_COMMAND $dom0_cluster_ip "$HVM_HELPER $1 $2 $stop_timeout" +} + +# Main code +case $1 in +gethosts) + echo $hostlist + exit 0 + ;; +on|off|reset|status) + RunHVMCommand $1 $2 + exit $? + ;; +getconfignames) + echo "hostlist dom0_cluster_ip timeout" + exit 0 + ;; +getinfo-devid) + echo "xen0-ha DomU/Dom0 device" + exit 0 + ;; +getinfo-devname) + echo "xen0-ha DomU/Dom0 external device" + exit 0 + ;; +getinfo-devdescr) + echo "Allows STONITH to control DomUs managed by a CRM/Pacemaker Dom0." + echo "Requires Xen + CRM/Pacemaker at both layers." + echo "Proof-of-concept code!" + exit 0 + ;; +getinfo-devurl) + echo "http://linux-ha.org/wiki/DomUClusters" + exit 0 + ;; +getinfo-xml) + cat << SSHXML +<parameters> +<parameter name="hostlist" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Hostlist +</shortdesc> +<longdesc lang="en"> +The list of controlled DomUs, separated by whitespace. +These must be configured as Xen RA resources with a name with a matching +id. +For example: "xen-1 xen-2 xen-3" +</longdesc> +</parameter> +<parameter name="dom0_cluster_ip" unique="1" required="1"> +<content type="string" /> +<shortdesc lang="en"> +Dom0 cluster ip +</shortdesc> +<longdesc lang="en"> +The cluster IP address associated with Dom0. +Root user must be able to ssh to that node. +</longdesc> +</parameter> +<parameter name="stop_timeout"> +<content type="integer" /> +<shortdesc lang="en"> +Stop timeout +</shortdesc> +<longdesc lang="en"> +The timeout, in seconds, for which to wait for Dom0 to report that the +DomU has been stopped, before aborting with a failure. +</longdesc> +</parameter> +</parameters> +SSHXML + exit 0 + ;; +*) + exit 1 + ;; +esac |