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