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