83c96d1fad2cda77e509f97ae629cbdf66dabb2b
[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         print "Running EXPECT: %s" % scriptname
361         locfg = command.CMD()
362
363         if 'ip' in args:
364             host = self.ip
365         else:
366             host = self.host
367
368         print args
369         if 'sequence' in args:
370             seq = " ".join(args['sequence'])
371             seq = "'%s'" % seq
372         else:
373             seq = ""
374
375         cmd_str = get_python_lib(1) + "/pcucontrol/models/exp/"
376         cmd = cmd_str + "%s %s %s '%s' %s %s %s"  % (
377                     scriptname, host, self.username, 
378                     self.password, args['dryrun'], args['model'], seq)
379         print cmd
380         cmd_out, cmd_err = locfg.run_noexcept("expect " + cmd)
381         return cmd_out.strip() + cmd_err.strip()
382
383     def reboot(self, node_port, dryrun):
384         if dryrun:
385             looking_for_fxn = 'pcu_test'
386         else:
387             looking_for_fxn = 'pcu_run'
388
389         # verify that the function exists.
390         if not hasattr(self, looking_for_fxn):
391             raise Exception("This model (%s) does not implement %s" % (self.model, looking_for_fxn))
392
393         print "found function %s in model %s" % (looking_for_fxn, self.model)
394         reboot_fxn = getattr(self, looking_for_fxn)
395         ret = self.catcherror(reboot_fxn, node_port, dryrun)
396
397         return ret
398
399     def run(self, node_port, dryrun):
400         """ This function is to be defined by the specific PCU instance.  """
401         raise Exception("This function is not implemented")
402
403
404 from pcucontrol.util import command
405 from pcucontrol.models import *
406
407 def pcu_name(pcu):
408     if pcu['hostname'] is not None and pcu['hostname'] is not "":
409         return pcu['hostname']
410     elif pcu['ip'] is not None and pcu['ip'] is not "":
411         return pcu['ip']
412     else:
413         return None
414
415 class Unknown(PCUControl):
416     supported_ports = [22,23,80,443,5869,9100,16992]
417
418 def model_to_object(modelname):
419     if modelname is None:
420         return ManualPCU 
421     if "AMT" in modelname:
422         return IntelAMT
423     elif "BayTech" in modelname:
424         return BayTech
425     elif "HPiLO" in modelname:
426         return HPiLO
427     elif "IPAL" in modelname:
428         return IPAL
429     elif "APC" in modelname:
430         return APCControl
431     elif "DRAC" in modelname:
432         return DRAC
433     elif "WTI" in modelname:
434         return WTIIPS4
435     elif "ePowerSwitch" in modelname:
436         return ePowerSwitchNew
437     elif "IPMI" in modelname:
438         return OpenIPMI
439     elif "BlackBoxPSMaverick" in modelname:
440         return BlackBoxPSMaverick
441     elif "PM211MIP" in modelname:
442         return PM211MIP
443     elif "ManualPCU" in modelname:
444         return ManualPCU 
445     else:
446         print "UNKNOWN model %s"%modelname
447         return Unknown
448
449 def reboot_api(node, pcu, testrun=False):
450     rb_ret = ""
451
452     try:
453         modelname = pcu['model']
454         if modelname:
455             # get object instance 
456             instance = eval('%s(pcu, verbose)' % modelname)
457             # get pcu port 
458             i = pcu['node_ids'].index(node['node_id'])
459             p = pcu['ports'][i]
460             # reboot
461             rb_ret = instance.reboot(p, testrun)
462         else:
463             rb_ret =  "No modelname in PCU record."
464         # TODO: how to handle the weird, georgetown pcus, the drac faults, and ilo faults
465     except Exception, err:
466         rb_ret = "Exception Model(%s): " % modelname 
467         rb_ret += str(err)
468
469     return rb_ret
470
471 def convert_oldmodelname_to_newmodelname(oldmodelname, pcu_id):
472     newmodelname = None
473     update = {    'AP79xx' : 'APCControl13p13',
474                 'Masterswitch' : 'APCControl13p13',
475                 'DS4-RPC' : 'BayTech',
476                 'IP-41x_IP-81x' : 'IPAL',
477                 'DRAC3' : 'DRAC',
478                 'DRAC4' : 'DRAC',
479                 'ePowerSwitch' : 'ePowerSwitchOld',
480                 'ilo2' : 'HPiLO',
481                 'ilo1' : 'HPiLO',
482                 'PM211-MIP' : 'PM211MIP',
483                 'AMT2.5' : 'IntelAMT',
484                 'AMT3.0' : 'IntelAMT',
485                 'WTI_IPS-4' : 'WTIIPS4',
486                 'unknown'  : 'ManualPCU',
487                 'DRAC5'    : 'DRAC',
488                 'ipmi'    : 'OpenIPMI',
489                 'bbsemaverick' : 'BlackBoxPSMaverick',
490                 'manualadmin'  : 'ManualPCU',
491     }
492
493     if oldmodelname in update:
494         newmodelname = update[oldmodelname]
495     else:
496         newmodelname = oldmodelname
497
498     if pcu_id in [1102,1163,1055,1111,1231,1113,1127,1128,1148]:
499         newmodelname = 'APCControl12p3'
500     elif pcu_id in [1110,86]:
501         newmodelname = 'APCControl1p4'
502     elif pcu_id in [1221,1225,1220,1192]:
503         newmodelname = 'APCControl121p3'
504     elif pcu_id in [1173,1240,47,1363,1405,1401,1372,1371]:
505         newmodelname = 'APCControl121p1'
506     elif pcu_id in [1056,1237,1052,1209,1002,1008,1013,1022]:
507         newmodelname = 'BayTechCtrlC'
508     elif pcu_id in [93]:
509         newmodelname = 'BayTechRPC3NC'
510     elif pcu_id in [1057]:
511         newmodelname = 'BayTechCtrlCUnibe'
512     elif pcu_id in [1012]:
513         newmodelname = 'BayTechRPC16'
514     elif pcu_id in [1089, 1071, 1046, 1035, 1118]:
515         newmodelname = 'ePowerSwitchNew'
516
517     return newmodelname
518
519 def reboot_test_new(nodename, values, verbose, dryrun):
520     rb_ret = ""
521     if 'plc_pcu_stats' in values:
522         values.update(values['plc_pcu_stats'])
523
524     try:
525         #modelname = convert_oldmodelname_to_newmodelname(values['model'], values['pcu_id'])
526         modelname = values['model']
527         if modelname:
528             object = eval('%s(values, verbose)' % modelname)
529             rb_ret = object.reboot(values[nodename], dryrun)
530         else:
531             rb_ret =  "Not_Run"
532         # TODO: how to handle the weird, georgetown pcus, the drac faults, and ilo faults
533     except ExceptionPort, err:
534         rb_ret = str(err)
535     except NameError, err:
536         rb_ret = str(err)
537
538     return rb_ret
539
540 def reboot_simple(nodeid, values, verbose, dryrun):
541     rb_ret = ""
542     try:
543         # Find the index of the given nodeid; use the same index in the ports list
544         port = values['ports'][values['node_ids'].index(nodeid)]
545         object = eval('%s(values, verbose)' % values['model'])
546         rb_ret = object.reboot(port, dryrun)
547         # TODO: how to handle the weird, georgetown pcus, the drac faults, and ilo faults?
548     except ExceptionPort, err:
549         rb_ret = str(err)
550     except NameError, err:
551         rb_ret = str(err)
552
553     return rb_ret
554
555 def main():
556     print "this does not work."
557
558 if __name__ == '__main__':
559     main()