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