Verify expect versions of DRAC, HPiLO and updates to APCControl, & OpenIPMI
[pcucontrol.git] / pcucontrol / reboot.py
1 #!/usr/bin/python
2 #
3 # Reboot specified nodes
4 #
5
6 import getpass, getopt
7 import os, sys
8 import xml, xmlrpclib
9 import errno, time, traceback
10 import urllib2
11 import urllib
12 import array, struct
13 import base64
14 from subprocess import PIPE, Popen
15 from distutils.sysconfig import get_python_lib; 
16 import subprocess
17 import pcucontrol.transports.ssh.pxssh as pxssh
18 import pcucontrol.transports.ssh.pexpect as pexpect
19 import socket
20
21
22
23 # Use our versions of telnetlib and pyssh
24 sys.path.insert(0, os.path.dirname(sys.argv[0]))
25 import pcucontrol.transports.telnetlib as telnetlib
26 sys.path.insert(0, os.path.dirname(sys.argv[0]) + "/pyssh")    
27 import pcucontrol.transports.pyssh as pyssh
28
29 # Event class ID from pcu events
30 #NODE_POWER_CONTROL = 3
31
32 # Monitor user ID
33 #MONITOR_USER_ID = 11142
34
35 import logging
36 verbose = 1
37 #dryrun = 0;
38
39 class ExceptionNoTransport(Exception): pass
40 class ExceptionNotFound(Exception): pass
41 class ExceptionPassword(Exception): pass
42 class ExceptionTimeout(Exception): pass
43 class ExceptionPrompt(Exception): pass
44 class ExceptionSequence(Exception): pass
45 class ExceptionReset(Exception): pass
46 class ExceptionPort(Exception): pass
47 class ExceptionUsername(Exception): pass
48
49
50
51 # PCU has model, host, preferred-port, user, passwd, 
52
53 # This is an object derived directly form the PLCAPI DB fields
54 class PCU(object):
55     def __init__(self, plc_pcu_dict):
56         for field in ['username', 'password', 'site_id', 
57                         'hostname', 'ip', 
58                         'pcu_id', 'model', 
59                         'node_ids', 'ports', ]:
60             if field in plc_pcu_dict:
61                 if type(u"") == type(plc_pcu_dict[field]):
62                     # NOTE: if is a unicode string, convert it.
63                     self.__setattr__(field, str(plc_pcu_dict[field]))
64                 else:
65                     self.__setattr__(field, plc_pcu_dict[field])
66             else:
67                 raise Exception("No such field %s in PCU object" % field)
68
69 # These are the convenience functions build around the PCU object.
70 class PCUModel(PCU):
71     def __init__(self, plc_pcu_dict):
72         PCU.__init__(self, plc_pcu_dict)
73         self.host = self.pcu_name()
74
75     def pcu_name(self):
76         if self.hostname is not None and self.hostname is not "":
77             return self.hostname
78         elif self.ip is not None and self.ip is not "":
79             return self.ip
80         else:
81             return None
82
83     def nodeidToPort(self, node_id):
84         if node_id in self.node_ids:
85             for i in range(0, len(self.node_ids)):
86                 if node_id == self.node_ids[i]:
87                     return self.ports[i]
88
89         raise Exception("No such Node ID: %d" % node_id)
90
91     def catcherror(self, function, node_port, dryrun):
92         try:
93             return function(node_port, dryrun)
94         except ExceptionNotFound, err:
95             return "Not found: " + str(err)
96         except ExceptionPassword, err:
97             return "Password exception: " + str(err)
98         except ExceptionTimeout, err:
99             return "Timeout exception: " + str(err)
100         except ExceptionUsername, err:
101             return "No username prompt: " + str(err)
102         except ExceptionSequence, err:
103             return "Sequence error: " + str(err)
104         except ExceptionPrompt, err:
105             return "Prompt exception: " + str(err)
106         except ExceptionNoTransport, err:
107             return "No Transport: " + str(err)
108         except ExceptionPort, err:
109             return "No ports exception: " + str(err)
110         except socket.error, err:
111             return "socket error: timeout: " + str(err)
112         except urllib2.HTTPError, err:
113             return "HTTPError: " + str(err)
114         except urllib2.URLError, err:
115             return "URLError: " + str(err)
116         except EOFError, err:
117             self.transport.close()
118             import traceback
119             traceback.print_exc()
120             return "EOF connection reset" + str(err)
121
122         except Exception, err:
123             #from monitor.common import email_exception
124             #email_exception(self.host)
125             raise Exception(err)
126
127 # This class captures the observed pcu records from FindBadPCUs.py
128 class PCURecord:
129     def __init__(self, pcu_record_dict):
130         for field in ['port_status', 
131                         'dns_status', 
132                         'entry_complete', ]:
133             if field in pcu_record_dict:
134                 if field == "reboot":
135                     self.__setattr__("reboot_str", pcu_record_dict[field])
136                 else:
137                     self.__setattr__(field, pcu_record_dict[field])
138             #else:
139             #    raise Exception("No such field %s in pcu record dict" % field)
140
141 class Transport:
142     TELNET = "telnet"
143     SSH    = "ssh"
144     HTTP   = "http"
145     HTTPS  = "https"
146     IPAL   = "ipal"
147     IPMI   = "ipmi"
148     DRAC   = "drac"
149     AMT    = "amt"
150
151     TELNET_TIMEOUT = 120
152
153     porttypemap = {
154             5869 : DRAC,
155             22 : SSH,
156             23 : TELNET,
157             443 : HTTPS,
158             80 :  HTTP,
159             9100 : IPAL,
160              623 : IPMI,
161             16992 : AMT,
162         }
163
164     def __init__(self, type, verbose):
165         self.type = type
166         self.verbose = verbose
167         self.transport = None
168
169     def open(self, host, username=None, password=None, prompt="User Name"):
170         transport = None
171
172         if self.type == self.TELNET:
173             transport = telnetlib.Telnet(host, timeout=self.TELNET_TIMEOUT)
174             transport.set_debuglevel(self.verbose)
175             if username is not None:
176                 self.transport = transport
177                 self.ifThenSend(prompt, username, ExceptionUsername)
178
179         elif self.type == self.SSH:
180             if username is not None:
181                 transport = pyssh.Ssh(username, host)
182                 transport.set_debuglevel(self.verbose)
183                 transport.open()
184                 # TODO: have an ssh set_debuglevel() also...
185             else:
186                 raise Exception("Username cannot be None for ssh transport.")
187         elif self.type == self.HTTP:
188             # NOTE: this does not work for all web-based services...
189             self.url = "http://%s:%d/" % (host,80)
190             uri = "%s:%d" % (host,80)
191
192             # create authinfo
193             authinfo = urllib2.HTTPPasswordMgrWithDefaultRealm()
194             authinfo.add_password (None, uri, username, password)
195             authhandler = urllib2.HTTPBasicAuthHandler( authinfo )
196
197             transport = urllib2.build_opener(authhandler)
198         else:
199             raise Exception("Unknown transport type: %s" % self.type)
200
201         self.transport = transport
202         return True
203
204     def close(self):
205         if self.type == self.TELNET:
206             self.transport.close() 
207         elif self.type == self.SSH:
208             self.transport.close() 
209         elif self.type == self.HTTP:
210             pass
211         else:
212             raise Exception("Unknown transport type %s" % self.type)
213         self.transport = None
214
215     def write(self, msg):
216         return self.send(msg)
217
218     def send(self, msg):
219         if self.transport == None:
220             raise ExceptionNoTransport("transport object is type None")
221             
222         return self.transport.write(msg)
223
224     def sendPassword(self, password, prompt=None):
225         if self.type == self.TELNET:
226             if prompt == None:
227                 self.ifThenSend("Password", password, ExceptionPassword)
228             else:
229                 self.ifThenSend(prompt, password, ExceptionPassword)
230         elif self.type == self.SSH:
231             self.ifThenSend("password:", password, ExceptionPassword)
232         elif self.type == self.HTTP:
233             pass
234         else:
235             raise Exception("Unknown transport type: %s" % self.type)
236
237     def sendHTTP(self, resource, data):
238         if self.verbose:
239             print "POSTing '%s' to %s" % (data,self.url + resource)
240
241         try:
242             f = self.transport.open(self.url + resource ,data)
243             r = f.read()
244             if self.verbose:
245                 print r
246
247         except urllib2.URLError,err:
248             print 'Could not open http connection', err
249             return "http transport error"
250
251         return 0
252
253     def ifThenSend(self, expected, buffer, ErrorClass=ExceptionPrompt):
254
255         if self.transport != None:
256             output = self.transport.read_until(expected, self.TELNET_TIMEOUT)
257             if output.find(expected) == -1:
258                 print "OUTPUT: --%s--" % output
259                 raise ErrorClass, "'%s' not found: Got: %s" % (expected, output)
260             else:
261                 self.transport.write(buffer + "\r\n")
262         else:
263             raise ExceptionNoTransport("transport object is type None")
264
265     def ifElse(self, expected, ErrorClass):
266         try:
267             self.transport.read_until(expected, self.TELNET_TIMEOUT)
268         except:
269             raise ErrorClass("Could not find '%s' within timeout" % expected)
270
271 class PCUControl(PCUModel,PCURecord):
272
273     """ 
274         There are three cases:
275             1) the pcu_record passed below includes port_status from an
276                 external probe.
277             2) the external probe failed, and the values are empty
278             3) this call is made independent of port_status.
279
280         In the first case, the first open port is used.
281         In the third case, the ports are tried in sequence.
282
283         In this way, the port_status value serves only as an optimization,
284         because closed ports are avoided.  The supported_ports value should
285         order ports by their preferred usage.
286     """
287
288     supported_ports = []
289
290     def __init__(self, plc_pcu_record, verbose, ignored=None):
291         PCUModel.__init__(self, plc_pcu_record)
292         PCURecord.__init__(self, plc_pcu_record)
293
294     def reboot(self, node_port, dryrun):
295
296         port_list = []
297         # There are two sources of potential ports.  Those that are open and
298         # those that are part of the PCU's supported_ports.  
299         #  I think we should start with supported_ports and then filter that
300         #  by the open ports.
301
302         port_list = self.supported_ports
303
304         if hasattr(self, 'port_status') and self.port_status:
305             # get out the open ports
306             port_list = filter(lambda x: self.port_status[x] == "open" , self.port_status.keys())
307             port_list = [ int(x) for x in port_list ]
308             # take only the open ports that are supported_ports
309             port_list = filter(lambda x: x in self.supported_ports, port_list)
310             if port_list == []:
311                 raise ExceptionPort("No Open Port: No transport from open ports")
312
313         print port_list
314
315         ret = "No implementation for open ports on selected PCU model"
316         for port in port_list:
317             if port not in Transport.porttypemap:
318                 continue
319
320             type = Transport.porttypemap[port]
321             self.transport = Transport(type, verbose)
322
323             print "checking for run_%s" % type
324             if hasattr(self, "run_%s" % type):
325                 print "found run_%s" % type
326                 fxn = getattr(self, "run_%s" % type)
327                 ret = self.catcherror(fxn, node_port, dryrun)
328                 if ret == 0: # NOTE: success!, so stop
329                     break
330             else:
331                 continue
332
333         return ret
334
335     def run(self, node_port, dryrun):
336         """ This function is to be defined by the specific PCU instance.  """
337         raise Exception("This function is not implemented")
338
339
340 class BasicPCUControl(PCUModel):
341
342     """ 
343         This is the basic pcucontrol super class on which the classes in
344         pcucontrol/models/* are based.
345
346         Each model implements a specific 'run' function that can use any
347         mechanism available to reboot the given PCU using, hostname, password,
348         username, and dryrun.
349
350         This is best suited for nodes with built-in PCUs, iLO, AMT, DRAC, etc.
351     """
352
353     supported_ports = []
354     install_path = get_python_lib(1) + "/pcucontrol/"
355
356     def __init__(self, plc_pcu_record, ignored=None):
357         PCUModel.__init__(self, plc_pcu_record)
358
359     def run_expect_script(self, scriptname, **args):
360         locfg = command.CMD()
361
362         cmd_str = get_python_lib(1) + "/pcucontrol/models/exp/"
363         cmd = cmd_str + "%s %s %s '%s' %s %s "  % (
364                     scriptname, self.host, self.username, 
365                     self.password, args['dryrun'], args['model'])
366         print cmd
367         cmd_out, cmd_err = locfg.run_noexcept(cmd)
368         return cmd_out.strip() + cmd_err.strip()
369
370     def reboot(self, node_port, dryrun):
371         if dryrun:
372             looking_for_fxn = 'pcu_test'
373         else:
374             looking_for_fxn = 'pcu_run'
375
376         # verify that the function exists.
377         if not hasattr(self, looking_for_fxn):
378             raise Exception("This model (%s) does not implement %s" % (self.model, looking_for_fxn))
379
380         print "found function %s in model %s" % (looking_for_fxn, self.model)
381         reboot_fxn = getattr(self, looking_for_fxn)
382         ret = self.catcherror(reboot_fxn, node_port, dryrun)
383
384         return ret
385
386     def run(self, node_port, dryrun):
387         """ This function is to be defined by the specific PCU instance.  """
388         raise Exception("This function is not implemented")
389
390
391 from pcucontrol.util import command
392 from pcucontrol.models import *
393
394 def pcu_name(pcu):
395     if pcu['hostname'] is not None and pcu['hostname'] is not "":
396         return pcu['hostname']
397     elif pcu['ip'] is not None and pcu['ip'] is not "":
398         return pcu['ip']
399     else:
400         return None
401
402 class Unknown(PCUControl):
403     supported_ports = [22,23,80,443,5869,9100,16992]
404
405 def model_to_object(modelname):
406     if modelname is None:
407         return ManualPCU 
408     if "AMT" in modelname:
409         return IntelAMT
410     elif "BayTech" in modelname:
411         return BayTech
412     elif "HPiLO" in modelname:
413         return HPiLO
414     elif "IPAL" in modelname:
415         return IPAL
416     elif "APC" in modelname:
417         return APCControl
418     elif "DRAC" in modelname:
419         return DRAC
420     elif "WTI" in modelname:
421         return WTIIPS4
422     elif "ePowerSwitch" in modelname:
423         return ePowerSwitchNew
424     elif "IPMI" in modelname:
425         return OpenIPMI
426     elif "BlackBoxPSMaverick" in modelname:
427         return BlackBoxPSMaverick
428     elif "PM211MIP" in modelname:
429         return PM211MIP
430     elif "ManualPCU" in modelname:
431         return ManualPCU 
432     else:
433         print "UNKNOWN model %s"%modelname
434         return Unknown
435
436 def reboot_api(node, pcu, testrun=False):
437     rb_ret = ""
438
439     try:
440         modelname = pcu['model']
441         if modelname:
442             # get object instance 
443             instance = eval('%s(pcu, verbose)' % modelname)
444             # get pcu port 
445             i = pcu['node_ids'].index(node['node_id'])
446             p = pcu['ports'][i]
447             # reboot
448             rb_ret = instance.reboot(p, testrun)
449         else:
450             rb_ret =  "No modelname in PCU record."
451         # TODO: how to handle the weird, georgetown pcus, the drac faults, and ilo faults
452     except Exception, err:
453         rb_ret = "Exception Model(%s): " % modelname 
454         rb_ret += str(err)
455
456     return rb_ret
457
458 def convert_oldmodelname_to_newmodelname(oldmodelname, pcu_id):
459     newmodelname = None
460     update = {    'AP79xx' : 'APCControl13p13',
461                 'Masterswitch' : 'APCControl13p13',
462                 'DS4-RPC' : 'BayTech',
463                 'IP-41x_IP-81x' : 'IPAL',
464                 'DRAC3' : 'DRAC',
465                 'DRAC4' : 'DRAC',
466                 'ePowerSwitch' : 'ePowerSwitchOld',
467                 'ilo2' : 'HPiLO',
468                 'ilo1' : 'HPiLO',
469                 'PM211-MIP' : 'PM211MIP',
470                 'AMT2.5' : 'IntelAMT',
471                 'AMT3.0' : 'IntelAMT',
472                 'WTI_IPS-4' : 'WTIIPS4',
473                 'unknown'  : 'ManualPCU',
474                 'DRAC5'    : 'DRAC',
475                 'ipmi'    : 'OpenIPMI',
476                 'bbsemaverick' : 'BlackBoxPSMaverick',
477                 'manualadmin'  : 'ManualPCU',
478     }
479
480     if oldmodelname in update:
481         newmodelname = update[oldmodelname]
482     else:
483         newmodelname = oldmodelname
484
485     if pcu_id in [1102,1163,1055,1111,1231,1113,1127,1128,1148]:
486         newmodelname = 'APCControl12p3'
487     elif pcu_id in [1110,86]:
488         newmodelname = 'APCControl1p4'
489     elif pcu_id in [1221,1225,1220,1192]:
490         newmodelname = 'APCControl121p3'
491     elif pcu_id in [1173,1240,47,1363,1405,1401,1372,1371]:
492         newmodelname = 'APCControl121p1'
493     elif pcu_id in [1056,1237,1052,1209,1002,1008,1013,1022]:
494         newmodelname = 'BayTechCtrlC'
495     elif pcu_id in [93]:
496         newmodelname = 'BayTechRPC3NC'
497     elif pcu_id in [1057]:
498         newmodelname = 'BayTechCtrlCUnibe'
499     elif pcu_id in [1012]:
500         newmodelname = 'BayTechRPC16'
501     elif pcu_id in [1089, 1071, 1046, 1035, 1118]:
502         newmodelname = 'ePowerSwitchNew'
503
504     return newmodelname
505
506 def reboot_test_new(nodename, values, verbose, dryrun):
507     rb_ret = ""
508     if 'plc_pcu_stats' in values:
509         values.update(values['plc_pcu_stats'])
510
511     try:
512         #modelname = convert_oldmodelname_to_newmodelname(values['model'], values['pcu_id'])
513         modelname = values['model']
514         if modelname:
515             object = eval('%s(values, verbose)' % modelname)
516             rb_ret = object.reboot(values[nodename], dryrun)
517         else:
518             rb_ret =  "Not_Run"
519         # TODO: how to handle the weird, georgetown pcus, the drac faults, and ilo faults
520     except ExceptionPort, err:
521         rb_ret = str(err)
522     except NameError, err:
523         rb_ret = str(err)
524
525     return rb_ret
526
527 def main():
528     print "this does not work."
529
530 if __name__ == '__main__':
531     main()