1#!/usr/local/bin/python3.8
2# vim: set filetype=python
3#######################################################################
4#
5# ibmrsa-telnet - External stonith plugin for HAv2 (http://linux-ha.org/wiki)
6#                 Connects to IBM RSA Board via telnet and switches power
7#                 of server appropriately.
8#
9# Author: Andreas Mock (andreas.mock@web.de)
10#
11# History:
12#   2007-10-19  Fixed bad commandline handling in case of stonithing
13#   2007-10-11  First release.
14#
15# Comment: Please send bug fixes and enhancements.
16#  I hope the functionality of communicating via telnet is encapsulated
17#  enough so that someone can use it for similar purposes.
18#
19# Description: IBM offers Remote Supervisor Adapters II for several
20#  servers. These RSA boards can be accessed in different ways.
21#  One of that is via telnet. Once logged in you can use 'help' to
22#  show all available commands. With 'power' you can reset, power on and
23#  off the controlled server. This command is used in combination
24#  with python's standard library 'telnetlib' to do it automatically.
25#
26# cib-snippet: Please see README.ibmrsa-telnet for examples.
27#
28# Copyright (c) 2007 Andreas Mock (andreas.mock@web.de)
29#                    All Rights Reserved.
30#
31# This program is free software; you can redistribute it and/or modify
32# it under the terms of version 2 or later of the GNU General Public
33# License as published by the Free Software Foundation.
34#
35# This program is distributed in the hope that it would be useful, but
36# WITHOUT ANY WARRANTY; without even the implied warranty of
37# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
38#
39# Further, this software is distributed without any warranty that it is
40# free of the rightful claim of any third person regarding infringement
41# or the like.  Any license provided herein, whether implied or
42# otherwise, applies only to this software file.  Patent licenses, if
43# any, provided herein do not apply to combinations of this program with
44# other software, or any other product whatsoever.
45#
46# You should have received a copy of the GNU General Public License
47# along with this program; if not, write the Free Software Foundation,
48# Inc., 59 Temple Place - Suite 330, Boston MA 02111-1307, USA.
49#
50#######################################################################
51import sys
52import os
53import time
54import telnetlib
55import subprocess
56
57class TimeoutException(Exception):
58    def __init__(self, value=None):
59        Exception.__init__(self)
60        self.value = value
61
62    def __str__(self):
63        return repr(self.value)
64
65class RSABoard(telnetlib.Telnet):
66    def __init__(self, *args, **kwargs):
67        telnetlib.Telnet.__init__(self, *args, **kwargs)
68        self._timeout = 10
69        self._loggedin = 0
70        self._history = []
71        self._appl = os.path.basename(sys.argv[0])
72
73    def _get_timestamp(self):
74        ct = time.time()
75        msecs = (ct - long(ct)) * 1000
76        return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S",
77                            time.localtime(ct)), msecs)
78
79    def write(self, buffer, nolog = False):
80        self._history.append(self._get_timestamp() + ': WRITE: ' +
81            (nolog and '******' or repr(buffer)))
82        telnetlib.Telnet.write(self, buffer)
83
84    def expect(self, what, timeout=20):
85        line = telnetlib.Telnet.expect(self, what, timeout)
86        self._history.append(self._get_timestamp() + ': READ : ' + repr(line))
87        if not line:
88            raise TimeoutException("Timeout while waiting for '%s'." % (what, ))
89        return line
90
91    def login(self, user, passwd):
92        time.sleep(1)
93        line = self.expect(['\nlogin : ', '\nusername: '], self._timeout)
94        self.write(user)
95        self.write('\r')
96        line = self.expect(['\nPassword: ', '\npassword: '], self._timeout)
97        self.write(passwd, nolog = True)
98        self.write('\r')
99        line = self.expect(['\nsystem>', '> '], self._timeout)
100
101    def reset(self):
102        self.write('power cycle\r')
103        line = self.expect(['\nok'], self._timeout)
104        line = self.expect(['\nsystem>', '> '], self._timeout)
105
106    def on(self):
107        self.write('power on\r')
108        line = self.expect(['\nok'], self._timeout)
109        line = self.expect(['\nsystem>', '> '], self._timeout)
110
111    def off(self):
112        self.write('power off\r')
113        line = self.expect(['\nok'], self._timeout)
114        line = self.expect(['\nsystem>', '> '], self._timeout)
115
116    def exit(self):
117        self.write('exit\r')
118
119    def get_history(self):
120        return "\n".join(self._history)
121
122
123class RSAStonithPlugin:
124    def __init__(self):
125        # define the external stonith plugin api
126        self._required_cmds = \
127            'reset gethosts status getconfignames getinfo-devid ' \
128            'getinfo-devname getinfo-devdescr getinfo-devurl ' \
129            'getinfo-xml'
130        self._optional_cmds = 'on off'
131        self._required_cmds_list = self._required_cmds.split()
132        self._optional_cmds_list = self._optional_cmds.split()
133
134        # who am i
135        self._appl = os.path.basename(sys.argv[0])
136
137        # telnet connection object
138        self._connection = None
139
140        # the list of configuration names
141        self._confignames = ['nodename', 'ip_address', 'username', 'password']
142
143        # catch the parameters provided by environment
144        self._parameters = {}
145        for name in self._confignames:
146            try:
147                self._parameters[name] = os.environ.get(name, '').split()[0]
148            except IndexError:
149                self._parameters[name] = ''
150
151    def _get_timestamp(self):
152        ct = time.time()
153        msecs = (ct - long(ct)) * 1000
154        return "%s,%03d" % (time.strftime("%Y-%m-%d %H:%M:%S",
155                            time.localtime(ct)), msecs)
156
157    def _echo_debug(self, *args):
158        self.echo_log('debug', *args)
159
160    def echo(self, *args):
161        what = ''.join([str(x) for x in args])
162        sys.stdout.write(what)
163        sys.stdout.write('\n')
164        sys.stdout.flush()
165        self._echo_debug("STDOUT:", what)
166
167    def echo_log(self, level, *args):
168        subprocess.call(('ha_log.sh', level) +  args)
169
170    def _get_connection(self):
171        if not self._connection:
172            c = RSABoard()
173            self._echo_debug("Connect to '%s'" %
174                  (self._parameters['ip_address'],))
175            c.open(self._parameters['ip_address'])
176            self._echo_debug("Connection established")
177            c.login(self._parameters['username'],
178                    self._parameters['password'])
179            self._connection = c
180
181    def _end_connection(self):
182        if self._connection:
183            self._connection.exit()
184            self._connection.close()
185
186    def reset(self):
187        self._get_connection()
188        self._connection.reset()
189        self._end_connection()
190        self._echo_debug(self._connection.get_history())
191        self.echo_log("info", "Reset of node '%s' done" %
192                              (self._parameters['nodename'],))
193        return(0)
194
195    def on(self):
196        self._get_connection()
197        self._connection.on()
198        self._end_connection()
199        self._echo_debug(self._connection.get_history())
200        self.echo_log("info", "Switched node '%s' ON" %
201                              (self._parameters['nodename'],))
202        return(0)
203
204    def off(self):
205        self._get_connection()
206        self._connection.off()
207        self._end_connection()
208        self._echo_debug(self._connection.get_history())
209        self.echo_log("info", "Switched node '%s' OFF" %
210                              (self._parameters['nodename'],))
211        return(0)
212
213    def gethosts(self):
214        self.echo(self._parameters['nodename'])
215        return(0)
216
217    def status(self):
218        self._get_connection()
219        self._end_connection()
220        self._echo_debug(self._connection.get_history())
221        return(0)
222
223    def getconfignames(self):
224        for name in ['nodename', 'ip_address', 'username', 'password']:
225            self.echo(name)
226        return(0)
227
228    def getinfo_devid(self):
229        self.echo("External Stonith Plugin for IBM RSA Boards")
230        return(0)
231
232    def getinfo_devname(self):
233        self.echo("External Stonith Plugin for IBM RSA Boards connecting "
234                  "via Telnet")
235        return(0)
236
237    def getinfo_devdescr(self):
238        self.echo("External stonith plugin for HAv2 which connects to "
239                  "a RSA board on IBM servers via telnet. Commands to "
240                  "turn on/off power and to reset server are sent "
241                  "appropriately. "
242                  "(c) 2007 by Andreas Mock (andreas.mock@web.de)")
243        return(0)
244
245    def getinfo_devurl(self):
246        self.echo("http://www.ibm.com/Search/?q=remote+supervisor+adapter")
247
248    def getinfo_xml(self):
249        info = """<parameters>
250            <parameter name="nodename" unique="1" required="1">
251                <content type="string" />
252                <shortdesc lang="en">nodename to shoot</shortdesc>
253                <longdesc lang="en">
254                Name of the node which has to be stonithed in case.
255                </longdesc>
256            </parameter>
257            <parameter name="ip_address" unique="1" required="1">
258                <content type="string" />
259                <shortdesc lang="en">hostname or ip address of RSA</shortdesc>
260                <longdesc lang="en">
261                Hostname or ip address of RSA board used to reset node.
262                </longdesc>
263            </parameter>
264            <parameter name="username" unique="1" required="1">
265                <content type="string" />
266                <shortdesc lang="en">username to login on RSA board</shortdesc>
267                <longdesc lang="en">
268                Username to login on RSA board.
269                </longdesc>
270            </parameter>
271            <parameter name="password" unique="1" required="1">
272                <content type="string" />
273                <shortdesc lang="en">password to login on RSA board</shortdesc>
274                <longdesc lang="en">
275                Password to login on RSA board.
276                </longdesc>
277            </parameter>
278        </parameters>
279        """
280        self.echo(info)
281        return(0)
282
283    def not_implemented(self, cmd):
284        self.echo_log("err", "Command '%s' not implemented." % (cmd,))
285        return(1)
286
287    def usage(self):
288        usage = "Call me with one of the allowed commands: %s, %s" % (
289        ', '.join(self._required_cmds_list),
290        ', '.join(self._optional_cmds_list))
291        return usage
292
293    def process(self, argv):
294        self._echo_debug("========== Start =============")
295        if len(argv) < 1:
296            self.echo_log("err", 'At least one commandline argument required.')
297            return(1)
298        cmd = argv[0]
299        self._echo_debug("cmd:", cmd)
300        if cmd not in self._required_cmds_list and \
301           cmd not in self._optional_cmds_list:
302            self.echo_log("err", "Command '%s' not supported." % (cmd,))
303            return(1)
304        try:
305            cmd = cmd.lower().replace('-', '_')
306            func = getattr(self, cmd, self.not_implemented)
307            rc = func()
308            return(rc)
309        except Exception, args:
310            self.echo_log("err", 'Exception raised:', str(args))
311            if self._connection:
312                self.echo_log("err", self._connection.get_history())
313                self._connection.close()
314            return(1)
315
316
317if __name__ == '__main__':
318    stonith = RSAStonithPlugin()
319    rc = stonith.process(sys.argv[1:])
320    sys.exit(rc)
321