Package DataCollector :: Module TelnetClient
[hide private]
[frames] | no frames]

Source Code for Module DataCollector.TelnetClient

  1  ########################################################################### 
  2  # 
  3  # This program is part of Zenoss Core, an open source monitoring platform. 
  4  # Copyright (C) 2007, Zenoss Inc. 
  5  # 
  6  # This program is free software; you can redistribute it and/or modify it 
  7  # under the terms of the GNU General Public License version 2 as published by 
  8  # the Free Software Foundation. 
  9  # 
 10  # For complete information please visit: http://www.zenoss.com/oss/ 
 11  # 
 12  ########################################################################### 
 13   
 14  __doc__="""TelnetClient 
 15   
 16  TelnetClient is used by TelnetClient to issue commands to a machine 
 17  and return their output over the telnet protocol. 
 18   
 19  Device Tree Parameters are: 
 20   
 21  zTelnetLoginTries - number of times to try login default: 1 
 22  zTelnetLoginTimeout - timeout for expect statements during login default: 2 
 23  zTelnetPromptTimeout - pause used during prompt discovery default: 0.2 
 24  zTelnetCommandTimeout - default timeout when executing a command default: 5 
 25  zTelnetLoginRegex - regex to match the login prompt default: 'ogin:.$' 
 26  zTelnetPasswordRegex - regext to match the password prompt default: 'assword:.$' 
 27   
 28  Other Parameters that are used by both TelnetClient and SshClient: 
 29  zCommandPathList - list of path to check for a command 
 30  zCommandExistanceCheck - shell command issued to look for executible 
 31                          must echo succ if executible is found 
 32                          default: test -f executible 
 33   
 34  """ 
 35   
 36  import Globals 
 37   
 38  #FIXME take away except when we are totally migrated 
 39  try: 
 40      from twisted.conch import telnet 
 41  except: 
 42      from twisted.protocols import telnet 
 43   
 44  from twisted.internet import reactor 
 45   
 46  import re 
 47  import logging 
 48  log = logging.getLogger("zen.TelnetClient") 
 49   
 50  import CollectorClient 
 51  from Exceptions import * 
 52   
 53  from Products.ZenUtils.Utils import unused 
 54   
 55  defaultPromptTimeout = 10  
 56  defaultLoginRegex = 'ogin:.$' 
 57  defaultPasswordRegex = 'assword:' 
 58  defaultEnable = False 
 59  defaultTermLength = False 
 60   
 61  responseMap = ("WILL", "WONT", "DO", "DONT") 
 62   
63 -def check(hostname):
64 """ 65 Check to see if a device supports telnet 66 67 @param hostname: name or IP address of device 68 @type hostname: string 69 @return: whether or not telnet port is available 70 @rtype: integer 71 @todo: support alternate ports 72 """ 73 from telnetlib import Telnet 74 import socket 75 try: 76 tn = Telnet(hostname) 77 tn.close() 78 return 1 79 except socket.error: 80 return 0
81 82
83 -class TelnetClientProtocol(telnet.Telnet):
84 """ 85 State-machine-based class for telnet 86 87 To switch from one state to the next, methods 88 return the next state. 89 """ 90 mode = 'Login' 91 92 timeout = 0 93 timeoutID = None 94 p1 = "" 95 p2 = "" 96 commandPrompt = "" 97 command = '' 98 enabled = -1 99 scCallLater = None 100 bytes = '' 101 lastwrite = '' 102 result = '' 103 buffer = "" 104
105 - def connectionMade(self):
106 """ 107 Called when a telnet session is established 108 """ 109 self.factory.myprotocol = self #bogus hack 110 self.hostname = self.factory.hostname 111 log.info("connected to device %s" % self.hostname) 112 self.startTimeout(self.factory.loginTimeout, self.loginTimeout) 113 self.protocol = telnet.TelnetProtocol()
114 115
116 - def iac_DO(self, feature):
117 """ 118 Do we support this telnet feature? 119 Reply back appropriately. 120 121 @param feature: IAC feature request 122 @type feature: string 123 """ 124 log.debug("Received telnet DO feature %s" % ord(feature)) 125 if ord(feature) == 1: 126 self._iac_response(telnet.WILL, feature) 127 else: 128 self._iac_response(telnet.WONT, feature)
129
130 - def iac_DONT(self, feature):
131 """ 132 Do we support this telnet feature? 133 Reply back appropriately. 134 135 @param feature: IAC feature request 136 @type feature: string 137 """ 138 # turn off telnet options 139 log.debug("Received telnet DONT feature %s" % ord(feature)) 140 self._iac_response(telnet.WONT, feature)
141 142
143 - def iac_WILL(self, feature):
144 """ 145 Do we support this telnet feature? 146 Reply back appropriately. 147 148 @param feature: IAC feature request 149 @type feature: string 150 """ 151 log.debug("Received telnet WILL feature %s" % ord(feature)) 152 # turn off telnet options 153 self._iac_response(telnet.DONT, feature)
154 155
156 - def iac_WONT(self, feature):
157 """ 158 Do we support this telnet feature? 159 Reply back appropriately. 160 161 @param feature: IAC feature request 162 @type feature: string 163 """ 164 log.debug("Received telnet WONT feature %s" % ord(feature)) 165 # turn off telnet options 166 self._iac_response(telnet.DONT, feature)
167 168
169 - def _iac_response(self, action, feature):
170 """ 171 Respond to IAC request with our response 172 173 @param action: IAC action 174 @type action: string 175 @param feature: IAC feature request 176 @type feature: string 177 """ 178 log.debug("Sending telnet action %s feature %s" % 179 (responseMap[ord(action)-251], ord(feature))) 180 self.write(telnet.IAC+action+feature)
181 182
183 - def write(self, data):
184 """ 185 Write data across the wire and record it. 186 187 @param data: data to write 188 @type data: string 189 """ 190 self.lastwrite = data 191 self.transport.write(data)
192 193
194 - def processChunk(self, chunk):
195 """ 196 Given data returned from the remote device, test out 197 the current chunk of data to determine whether 198 or not to switch states, or just add the chunk to 199 the list of data received from the host. 200 201 If we find the end condition for the state, process 202 the line. 203 204 @param chunk: data 205 @type chunk: string 206 """ 207 self.buffer = self.buffer + chunk 208 regex = None 209 if self.factory.modeRegex.has_key(self.mode): 210 regex = self.factory.modeRegex[self.mode] 211 log.debug("Mode '%s' regex = %s" % (self.mode, regex)) 212 log.debug("Chunk received = '%s'" % chunk) 213 if regex and re.search(regex, chunk): 214 self.processLine(self.buffer) 215 self.buffer = ""
216 217
218 - def processLine(self, line):
219 """ 220 Call a method that looks like 'telnet_*' where '*' is filled 221 in by the current mode. telnet_* methods should return a string which 222 will become the new mode. 223 224 @param line: data 225 @type line: string 226 """ 227 line = re.sub("\r\n|\r", "\n", line) #convert \r\n to \n 228 #if server is echoing take it out 229 if self.lastwrite.startswith(line): 230 self.lastwrite = self.lastwrite[len(line):] 231 line = '' 232 elif line.find(self.lastwrite) == 0: 233 line = line[len(self.lastwrite):] 234 log.debug("mode = %s", self.mode) 235 self.mode = getattr(self, "telnet_"+self.mode)(line)
236 237
238 - def dataReceived(self, data):
239 """ 240 Look for data and send to processLine() 241 242 @param data: output from telnet 243 @type data: string 244 """ 245 telnet.Telnet.dataReceived(self, data) 246 log.debug('Line %r', self.bytes) 247 if self.bytes: 248 self.processLine(self.bytes) 249 self.bytes = ''
250 251
252 - def applicationDataReceived(self, bytes):
253 """ 254 Store any bytes received 255 256 @param bytes: output from telnet 257 @type bytes: string 258 """ 259 self.bytes += bytes
260 261
262 - def startTimeout(self, timeout=1, timeoutfunc=None):
263 """ 264 Start a timer to decide if we continue or not. 265 266 @param timeout: time in seconds to wait 267 @type timeout: integer 268 @param timeoutfunc: override for the default timeout timer 269 @type timeoutfunc: function 270 """ 271 self.cancelTimeout() 272 if timeoutfunc is None: timeoutfunc = self.defaultTimeout 273 self.timeoutID = reactor.callLater(timeout, timeoutfunc)
274 275
276 - def cancelTimeout(self):
277 """ 278 Cancel the timeout timer 279 """ 280 if self.timeoutID: self.timeoutID.cancel() 281 self.timeoutID = None
282 283
284 - def defaultTimeout(self):
285 """ 286 Reset the timeout timer 287 """ 288 self.transport.loseConnection() 289 if self.factory.commandsFinished(): 290 self.factory.clientFinished() 291 regex = self.factory.modeRegex.get(self.mode, "") 292 log.warn("Dropping connection to %s: " 293 "state '%s' timeout %.1f seconds regex '%s' buffer '%s'", 294 self.factory.hostname, self.mode, self.timeout,regex,self.buffer)
295 296 297
298 - def loginTimeout(self, loginTries=0):
299 """ 300 Called when the timeout timer expires. 301 302 @param loginTries: number of login failures to accept 303 @type loginTries: integer 304 @return: next state (Done, Login) 305 @rtype: string 306 """ 307 if loginTries == 0: 308 loginTries = self.factory.loginTries 309 310 elif loginTries == 1: 311 self.transport.loseConnection() 312 self.factory.clientFinished() 313 log.warn("Login to device %s failed" % self.hostname) 314 return "Done" 315 316 else: 317 self.factory.loginTries -= 1 318 return "Login"
319 320
321 - def telnet_Login(self, data):
322 """ 323 Called when login prompt is expected 324 325 @param data: data sent back from the remote device 326 @type data: string 327 @return: next state (Login, Password) 328 @rtype: string 329 """ 330 log.debug('Search for login regex (%s) in (%s) finds: %r' % \ 331 (self.factory.loginRegex, data, \ 332 re.search(self.factory.loginRegex, data))) 333 if not re.search(self.factory.loginRegex, data): # login failed 334 return 'Login' 335 log.debug("Login tries=%s" % self.factory.loginTries) 336 if not self.factory.loginTries: 337 self.transport.loseConnection() 338 log.warn("Login to %s with username %s failed" % ( 339 self.factory.hostname, self.factory.username)) 340 else: 341 self.factory.loginTries -= 1 342 log.debug("Sending username %s" % self.factory.username) 343 self.write(self.factory.username + '\n') 344 return 'Password'
345 346
347 - def telnet_Password(self, data):
348 """ 349 Called when the password prompt is expected 350 351 @param data: data sent back from the remote device 352 @type data: string 353 @return: next state (Password, FindPrompt) 354 @rtype: string 355 """ 356 log.debug('Search for password regex (%s) in (%s) finds: %r' % \ 357 (self.factory.passwordRegex, data, \ 358 re.search(self.factory.loginRegex, data))) 359 if not re.search(self.factory.passwordRegex, data): # look for pw prompt 360 return 'Password' 361 log.debug("Sending password %s" % self.factory.password) 362 self.write(self.factory.password + '\n') 363 self.startTimeout(self.factory.promptTimeout) 364 return 'FindPrompt'
365 366
367 - def telnet_Enable(self, unused):
368 """ 369 Switch to 'enable' mode on a Cisco device 370 371 @param unused: unused (unused) 372 @type unused: string 373 @return: next state (Password) 374 @rtype: string 375 """ 376 self.write('enable\n') 377 self.startTimeout(self.factory.loginTimeout, self.loginTimeout) 378 return "Password"
379 380
381 - def telnet_FindPrompt(self, data):
382 """ 383 Called after login to figure out the command prompt 384 385 @param data: data sent back from the remote device 386 @type data: string 387 @return: next state (ClearPromptData, FindPrompt, Password) 388 @rtype: string 389 """ 390 if not data.strip(): return 'FindPrompt' 391 if re.search(self.factory.loginRegex, data): # login failed 392 return self.telnet_Login(data) 393 self.p1 = data 394 if self.p1 == self.p2: 395 self.cancelTimeout() # promptTimeout 396 self.commandPrompt = self.p1 397 log.debug("found command prompt '%s'" % self.p1) 398 self.factory.modeRegex['Command'] = re.escape(self.p1) + "$" 399 self.factory.modeRegex['SendCommand'] = re.escape(self.p1) + "$" 400 if self.factory.enable: 401 self.factory.enable = False 402 # NB: returns Password 403 return self.telnet_Enable("") 404 else: 405 self.scCallLater = reactor.callLater(1.0, 406 self.telnet_SendCommand, "") 407 return "ClearPromptData" 408 self.p2 = self.p1 409 self.p1 = "" 410 log.debug("sending \\n") 411 reactor.callLater(.1, self.write, "\n") 412 return 'FindPrompt'
413 414
415 - def telnet_ClearPromptData(self, unused):
416 """ 417 Called to try to restore sanity to output from the user. 418 Send an empty string to get back a prompt 419 420 @param unused: unused (unused) 421 @type unused: string 422 @return: next state (ClearPromptData) 423 @rtype: string 424 """ 425 if self.scCallLater: self.scCallLater.cancel() 426 self.scCallLater = reactor.callLater(1.0, self.telnet_SendCommand, "") 427 return "ClearPromptData"
428 429
430 - def telnet_SendCommand(self, unused):
431 """ 432 Get a command of the command stack and send it 433 434 @param unused: unused (unused) 435 @type unused: string 436 @return: next state (Command) 437 @rtype: string 438 """ 439 if self.scCallLater and self.scCallLater.active(): 440 self.scCallLater.cancel() 441 log.debug("sending command '%s'" % self.curCommand()) 442 self.write(self.curCommand() + '\n') 443 self.startTimeout(self.factory.commandTimeout) 444 self.mode = 'Command' 445 return 'Command'
446 447
448 - def telnet_Command(self, data):
449 """ 450 Process the data from a sent command 451 If there are no more commands move to final state 452 453 @param data: data sent back from the remote device 454 @type data: string 455 @return: next state (Command, Done) 456 @rtype: string 457 """ 458 self.result += data 459 if not self.result.endswith(self.commandPrompt): 460 log.debug("Prompt '%s' not found", self.commandPrompt) 461 log.debug("Line ends wth '%s'", data[-5:]) 462 return 'Command' 463 self.cancelTimeout() 464 data, self.result = self.result, '' 465 log.debug("command = %s" % self.curCommand()) 466 log.debug("data=%s" % data) 467 self.factory.addResult(self.curCommand(), data[0:-len(self.p1)], None) 468 self.factory.cmdindex += 1 469 if self.factory.commandsFinished(): 470 self.factory.clientFinished() 471 if not self.factory.maintainConnection: 472 self.transport.loseConnection() 473 return 'Done' 474 else: 475 # Command 476 return self.telnet_SendCommand("")
477 478
479 - def curCommand(self):
480 """ 481 Return the current command to run 482 483 @return: next command to run 484 @rtype: string 485 """ 486 return self.factory._commands[self.factory.cmdindex]
487 488 489
490 -class TelnetClient(CollectorClient.CollectorClient):
491 """ 492 Reactor code to start communications and invoke our 493 telnet transport mechanism. 494 """ 495
496 - def __init__(self, hostname, ip, port, plugins=[], options=None, 497 device=None, datacollector=None):
498 """ 499 Initializer 500 501 @param hostname: hostname of the device 502 @type hostname: string 503 @param ip: IP address of the device 504 @type ip: string 505 @param port: port number to use to connect to device 506 @type port: integer 507 @param plugins: plugins 508 @type plugins: list of plugins 509 @param options: options 510 @type options: list 511 @param device: name of device 512 @type device: string 513 @param datacollector: object 514 @type datacollector: object 515 """ 516 CollectorClient.CollectorClient.__init__(self, hostname, ip, port, 517 plugins, options, device, datacollector) 518 global defaultPromptTimeout 519 global defaultLoginRegex 520 global defaultPasswordRegex 521 global defaultEnable 522 523 self.protocol = TelnetClientProtocol 524 self.modeRegex = { 525 'FindPrompt' : '.*', 526 'WasteTime' : '.*', 527 'Done' : '', 528 } 529 self.promptPause = 1 530 531 if options: 532 defaultPromptTimeout = options.promptTimeout 533 defaultLoginRegex = options.loginRegex 534 defaultPasswordRegex = options.passwordRegex 535 defaultEnable = options.enable 536 537 if device: # if we are in Zope look for zProperties 538 self.promptTimeout = getattr(device, 539 'zTelnetPromptTimeout', defaultPromptTimeout) 540 self.loginRegex = getattr(device, 541 'zTelnetLoginRegex', defaultLoginRegex) 542 self.passwordRegex = getattr(device, 543 'zTelnetPasswordRegex', defaultPasswordRegex) 544 self.enable = getattr(device, 545 'zTelnetEnable', defaultEnable) 546 self.termlen = getattr(device, 547 'zTelnetTermLength', defaultTermLength) 548 549 else: 550 self.promptTimeout = defaultPromptTimeout 551 self.loginRegex = defaultLoginRegex 552 self.passwordRegex = defaultPasswordRegex 553 self.enable = defaultEnable 554 self.termlen = defaultTermLength 555 556 self.modeRegex['Login'] = self.loginRegex 557 self.modeRegex['Password'] = self.passwordRegex
558 559
560 - def run(self):
561 """ 562 Start telnet collection. 563 """ 564 if self.termlen: 565 self._commands.insert(0, "terminal length 0") 566 reactor.connectTCP(self.ip, self.port, self)
567 568
569 - def Command(self, commands):
570 """ 571 Add new commands to be run reset cmdindex to 0 572 573 @param commands: commands to run on the remote device 574 @type commands: list of commands 575 """ 576 CollectorClient.CollectorClient.addCommand(self, commands) 577 if self.myprotocol.mode != "Command": 578 self.myprotocol.telnet_SendCommand("")
579 580
581 - def clientConnectionFailed(self, connector, reason):
582 """ 583 If we don't connect let the modeler know 584 585 @param connector: unused (unused) 586 @type connector: unused 587 @param reason: error message to report 588 @type reason: string 589 """ 590 unused(connector) 591 log.warn(reason.getErrorMessage()) 592 self.clientFinished()
593 594 595
596 -def buildOptions(parser=None, usage=None):
597 """ 598 Command-line telnet options 599 """ 600 601 parser = CollectorClient.buildOptions(parser,usage) 602 603 parser.add_option('-r', '--promptTimeout', 604 dest='promptTimeout', 605 type = 'float', 606 default = defaultPromptTimeout, 607 help='Timeout when discovering prompt') 608 parser.add_option('-x', '--loginRegex', 609 dest='loginRegex', 610 default = defaultLoginRegex, 611 help='Python regular expression that will find the login prompt') 612 parser.add_option('-w', '--passwordRegex', 613 dest='passwordRegex', 614 default = defaultPasswordRegex, 615 help='Python regex that will find the password prompt') 616 parser.add_option('--enable', 617 dest='enable', action='store_true', default=False, 618 help="Enter 'enable' mode on a Cisco device") 619 parser.add_option('--termlen', 620 dest='termlen', action='store_true', default=False, 621 help="Enter 'send terminal length 0' on a Cisco device") 622 return parser
623 624
625 -class FakePlugin(object):
626 """ 627 Fake class to provide plugin instances for command-line processing. 628 """
629 - def __init__( self, command='' ):
630 self.command= command
631
632 - def __repr__( self ):
633 return "'%s'" % self.command
634 635
636 -def commandsToPlugins( commands ):
637 """ 638 The TelntClient class expects plugins. 639 Convert commands like 'ls a', 'ls b' to plugin instances. 640 Duplicate commands will (eventually) be removed. 641 This is used to support command-line arguments. 642 643 @param commands: list of commands from command-line 644 @type commands: list of strings 645 @return: list of commands, plugin-style 646 @rtype: list of FakePlugins 647 """ 648 return [ FakePlugin( cmd ) for cmd in commands ]
649 650
651 -def main():
652 """ 653 Test harness main() 654 655 Usage: 656 657 python TelnetClient.py hostname[:port] comand [command] 658 659 Each command must be enclosed in quotes (") to be interpreted 660 properly as a complete unit. 661 """ 662 import socket 663 import getpass 664 import pprint 665 666 parser = buildOptions() 667 options = CollectorClient.parseOptions(parser, 23) 668 if not options.password: 669 options.password = getpass.getpass("%s@%s's password: " % 670 (options.username, options.hostname)) 671 logging.basicConfig() 672 log.setLevel(options.logseverity) 673 commands = commandsToPlugins( options.commands ) 674 client = TelnetClient(options.hostname, 675 socket.gethostbyname(options.hostname), 676 options.port, 677 plugins=commands, options=options) 678 client.run() 679 client.clientFinished= reactor.stop 680 681 reactor.run() 682 683 pprint.pprint(client.getResults())
684 685 if __name__ == '__main__': 686 main() 687