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

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