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