#!/usr/bin/env python
# vim: set filetype=python
#######################################################################
#
# dracmc-telnet - External stonith plugin for HAv2 (http://www.linux-ha.org)
#                 Connects to Dell Drac/MC Blade Enclosure via a Cyclades
#                 terminal server with telnet and switches power of named
#                 blade servers appropriatelly.
#
# 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.
#
# Description:
#     Dell offers the Drac/MC in their blade enclosures.  The
# Drac/MC can be accessed in different ways.  One way to interface to it
# is to connect the blade enclosure's Drac/MC serial port to a Cyclades
# terminal server.  You can then access the Drac/MC via telnet via the
# Cyclades.  Once logged in, you can use 'help' to show all available
# commands.  With the 'serveraction' command, you can control both
# hard and soft resets as well as power to a particular blade.  The
# blades are named 'Server-X', where 'X' is a number which corresponds
# to the blade number in the enclosure.  This plugin allows using the
# Drac/MC with stonith.  It uses python's standards 'telnetlib' library
# to log in and issue commands.  The code is very similar to the original
# ibmrsa-telnet plugin released by Andreas and was quite easy to
# modify for this application.
#     One complication is that the Cyclades only allows one active
# connection.  If someone or something has a connection active, the
# terminal server closes the new attempted connection.  Since this
# situation can be common, for example if trying to stonith two blades
# or when the plugin is started by multiple cluster nodes, there is a
# built in retry mechanism for login.  On 10 retries, the code gives up
# and throws.
#     When running this resource, it is best to not run it as a clone,
# rather as a normal, single-instance resource.  Make sure to create a
# location constraint that excludes the node that is to be fenced.
#
#  For example:
#
#    primitive fence_node1 stonith:external/dracmc-telnet \
#             nodename=node1 cyclades_ip=10.0.0.1 cyclades_port=7001 \
#             servername=Server-1 username=USERID password=PASSWORD \
#             op monitor interval="200m" timeout="60s"
#    location loc-fence_node1 fence_node1 -inf: node1
#    commit
#
# 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
#
# cib-snippet:
#   See end of this file for a xml snippet to configure this
#   stonith device.
#
#
# 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

LOGFILE = False
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 _open_debug_file(self):
        try:
            self._f = file('/var/log/dracmc-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', '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] = ''

        # 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.
        if LOGFILE: 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 = 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("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", 'At least 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 = DracMCStonithPlugin()
    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 the DRAC/MC. However, if this node becomes crazy, then it is
#  not guranteed that a self-stonith will be successful.
#
#  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/dracmc-telnet" provider="heartbeat" resource_stickiness="0">
#                <operations>
#                    <op name="monitor" interval="200m" timeout="60s" 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-cyclades_ip" name="cyclades_ip" value="192.168.0.1"/>
#                        <nvpair id="r_stonith-node01-cyclades_port" name="cyclades_port" value="7032"/>
#                        <nvpair id="r_stonith-node01-servername" name="servername" value="Server-7"/>
#                        <nvpair id="r_stonith-node01-username" name="username" value="USERID"/>
#                        <nvpair id="r_stonith-node01-password" name="password" value="PASSWORD"/>
#                        <nvpair id="r_stonith-node01-type" name="type" value="dellblade"/>
#                    </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>
#
