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

# Source Code for Module Products.DataCollector.TelnetClient

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