#!/usr/bin/python
# vim: set filetype=python
#######################################################################
#
# ibmrsa-telnet - External stonith plugin for HAv2 (http://www.linux-ha.org/)
#                 Connects to IBM RSA Board via telnet and switches power
#                 of server appropriately.
#
# Author: Andreas Mock (andreas.mock@web.de)
#
# History:
#   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:
#   See end of this file for a xml snippet to configure this
#   stonith device.
#
#
# 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 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

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 = 3
        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):
        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):
        self.write("\r")
        line = self.read_until('username: ', self._timeout)
        self.write(user)
        self.write('\r')
        line = self.read_until('password: ', self._timeout)
        self.write(passwd)
        self.write('\r')
        line = self.read_until('> ', self._timeout)

    def reset(self):
        self.write('power cycle\r')
        line = self.read_until('ok', 10)
        line = self.read_until('> ', self._timeout)

    def on(self):
        self.write('power on\r')
        line = self.read_until('ok', 10)
        line = self.read_until('> ', self._timeout)

    def off(self):
        self.write('power off\r')
        line = self.read_until('ok', 10)
        line = self.read_until('> ', self._timeout)

    def exit(self):
        self.write('exit\r')

    def get_history(self):
        return "\n".join(self._history)


class RSAStonithPlugin:
    def _open_debug_file(self):
        try:
            self._f = file('/var/log/stonith-debug.log', 'a')
        except:
            pass

    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] = ''

        # enable debugging log file if you need
        self._f = None   # File Object for Debugging
        # uncomment the next line if you want to have a  debug file.
        # WARNING: Only for debugging. The way how the file is opened
        # is NOT secure.
        #self._open_debug_file()

    def __del__(self):
        if self._f:
            self._f.close()

    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):
        f = self._f
        if f:
            f.write("%s: %s: %s\n" % (self._get_timestamp(), self._appl,
                    ' '.join(args)))
            f.flush

    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):
        what = "%s: %s: %s" % (level, self._appl, ' '.join(args))
        self.echo(what)
        self._echo_debug(what)

    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'])
            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("ERROR", "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("ERROR", 'Exactly one commandline argument required.')
            self.echo(self.usage())
            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("ERROR", "Command '%s' not supported." % (cmd,))
            self.echo(self.usage())
            return(1)
        try:
            cmd = cmd.lower().replace('-', '_')
            func = getattr(self, cmd, self.not_implemented)
            rc = func()
            return(rc)
        except Exception, args:
            self.echo_log("ERROR", 'Exception raised:', str(args))
            if self._connection:
                self.echo_log(self._connection.get_history())
                self._connection.close()
            return(1)


if __name__ == '__main__':
    stonith = RSAStonithPlugin()
    rc = stonith.process(sys.argv[1:])
    sys.exit(rc)


##############################################################################
# Code snippet for cib
#  It's useful to give a location preference so that the stonith agent
#  is run on the/an other node. This is not necessary as one node can kill
#  itself via RSA Board. But: If this node becomes crazy my experiences
#  showed that the node is not able to shoot itself anymore properly.
#
#  You have to adjust parameters, scores and timeout values to fit your
#  HA environment.
##############################################################################
#<?xml version="1.0" ?>
#<cib>
#    <configuration>
#        <resources>
#            <primitive id="r_stonith-node01" class="stonith" type="external/ibmrsa" provider="heartbeat" resource_stickiness="0">
#                <operations>
#                    <op name="monitor" interval="60" timeout="300" prereq="nothing" id="r_stonith-node01-mon"/>
#                    <op name="start" timeout="180" id="r_stonith-node01-start"/>
#                    <op name="stop" timeout="180" id="r_stonith-node01-stop"/>
#                </operations>
#                <instance_attributes id="r_stonith-node01">
#                    <attributes>
#                        <nvpair id="r_stonith-node01-nodename" name="nodename" value="node01"/>
#                        <nvpair id="r_stonith-node01-ipaddr" name="ipaddr" value="192.168.0.1"/>
#                        <nvpair id="r_stonith-node01-userid" name="userid" value="userid"/>
#                        <nvpair id="r_stonith-node01-passwd" name="passwd" value="password"/>
#                        <nvpair id="r_stonith-node01-type" name="type" value="ibm"/>
#                    </attributes>
#                </instance_attributes>
#            </primitive>
#        </resources>
#        <constraints>
#            <rsc_location id="r_stonith-node01_prefer_node02" rsc="r_stonith-node01">
#                <rule id="r_stonith-node01_prefer_node02_rule" score="50">
#                    <expression attribute="#uname" id="r_stonith-node01_prefer_node02_expr" operation="eq" value="node02"/>
#                </rule>
#            </rsc_location>
#        </constraints>
#
#    </configuration>
#</cib>
#
