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