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

# Source Code for Module Products.DataCollector.TelnetClient

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