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

Source Code for Module Products.DataCollector.TelnetClient

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