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

# Source Code for Module Products.DataCollector.SshClient

      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__="""SshClient runs commands on a remote box using SSH and
     15  returns their results.
     16
     17  See http://twistedmatrix.com/trac/wiki/Documentation for Twisted documentation,
     18  specifically documentation on 'conch' (Twisted's SSH protocol support).
     19  """
     20
     21  import os
     22  import sys
     23  from pprint import pformat
     24  import logging
     25  log = logging.getLogger("zen.SshClient")
     26  import socket
     27
     28  import Globals
     29
     30  from twisted.conch.ssh import transport, userauth, connection
     31  from twisted.conch.ssh import common, channel
     32  from twisted.conch.ssh.keys import Key
     33  from twisted.internet import defer, reactor
     34  from Products.ZenEvents import Event
     35  from Products.ZenUtils.Utils import getExitMessage
     36  from Products.ZenUtils.IpUtil import getHostByName
     37
     38  from Exceptions import *
     39
     40  import CollectorClient
     41
     42  # NB: Most messages returned back from Twisted are Unicode.
     43  #     Expect to use str() to convert to ASCII before dumping out. :)
     44
     45
     46 -def sendEvent( self, message="", device='', severity=Event.Error ):
     47      """
     48      Shortcut version of sendEvent()
     49
     50      @param message: message to send in Zenoss event
     51      @type message: string
     52      @param device: hostname of device to which this event is associated
     53      @type device: string
     54      @param severity: Zenoss severity from Products.ZenEvents
     55      @type severity: integer
     56      """
     57
     58      # Parse out the daemon's name
     59      component= os.path.basename( sys.argv[0] ).replace( '.py', '' )
     60
     61      def hasattr_path( object_root, path ):
     62          """
     63          The regular hasattr() only works on one component,
     64          not multiples.
     65
     66          @param object_root: object to start searching for path
     67          @type object_root: object
     68          @param path: path to func or variable (eg "conn.factory" )
     69          @type path: string
     70          @return: is object_root.path sane?
     71          @rtype: boolean
     72          """
     73          obj = object_root
     74          for chunk in path.split('.'):
     75              obj= getattr( obj, chunk, None )
     76              if obj is None:
     77                  return False
     78          return True
     79
     80      # ... and the device's name (as known by Zenoss)
     81      if device == '':
     82         if hasattr_path( self, "factory.hostname" ):
     83             device= self.factory.hostname
     84
     85         elif hasattr_path( self, "conn.factory.hostname" ):
     86             device= self.conn.factory.hostname
     87
     88         else:
     89             log.debug( "Couldn't get the remote device's hostname" )
     90
     91      error_event= {
     92         'agent': component,
     93         'summary': message,
     94         'device': device,
     95         'eventClass': "/Cmd/Fail",
     96         'component': component,
     97         'severity': severity,
     98      }
     99
    100      # At this point, we don't know what we have
    101      try:
    102          if hasattr_path( self, "factory.datacollector.sendEvent" ):
    103              self.factory.datacollector.sendEvent( error_event )
    104
    105          elif hasattr_path( self, "factory.sendEvent" ):
    106              self.factory.sendEvent( error_event )
    107
    108          elif hasattr_path( self, "datacollector.sendEvent" ):
    109              self.datacollector.sendEvent( error_event )
    110
    111          elif hasattr_path( self, "conn.factory.datacollector.sendEvent" ):
    112              self.conn.factory.datacollector.sendEvent( error_event )
    113
    114          else:
    115              log.debug( "Unable to send event for %s" % error_event )
    116
    117      except:
    118          pass # Don't cause other issues
    119
    120
    121
    122 -class SshClientError( Exception ):
    123      """
    124      Exception class
    125      """
    126
    127
    128
    129 -class SshClientTransport(transport.SSHClientTransport):
    130      """
    131      Base client class for constructing Twisted Conch services.
    132      This class is *only* responsible for connecting to the SSH
    133      service on the device, and ensuring that *host* keys are sane.
    134      """
    135
    136 -    def verifyHostKey(self, hostKey, fingerprint):
    137          """
    138          Module to verify the host's SSH key against the stored fingerprint we have
    139          from the last time that we communicated with the host.
    140
    141          NB: currently does not verify this information but simply trusts every host key
    142
    143          @param hostKey: host's SSH key (unused)
    144          @type hostKey: string
    145          @param fingerprint: host fingerprint (unused)
    146          @type fingerprint: string
    147          @return: Twisted deferred object
    148          @rtype: Twisted deferred object (defer.succeed(1)
    149          @todo: verify the host key
    150          """
    151          #blowing off host key right now, should store and check
    152          from Products.ZenUtils.Utils import unused
    153          unused(hostKey)
    154          log.debug('%s host fingerprint: %s' % (self.factory.hostname, fingerprint))
    155          return defer.succeed(1)
    156
    157
    158 -    def connectionMade(self):
    159          """
    160          Called after the connection has been made.
    161          Used to set up private instance variables.
    162          """
    163          self.factory.transport = self.transport
    164          transport.SSHClientTransport.connectionMade(self)
    165
    166
    167 -    def receiveError( self, reasonCode, description ):
    168          """
    169          Called when a disconnect error message was received from the device.
    170
    171          @param reasonCode: error code from SSH connection failure
    172          @type reasonCode: integer
    173          @param description: human-readable version of the error code
    174          @type description: string
    175          """
    176          message= 'SSH error from remote device (code %d): %s\n' % \
    177                   ( reasonCode, str( description ) )
    178          log.warn( message )
    179          sendEvent( self, message=message )
    180          transport.SSHClientTransport.receiveError(self, reasonCode, description )
    181
    182
    183 -    def receiveUnimplemented( self, seqnum ):
    184          """
    185          Called when an unimplemented packet message was received from the device.
    186
    187          @param seqnum: SSH message code
    188          @type seqnum: integer
    189          """
    190          message= "Got 'unimplemented' SSH message, seqnum= %d" % seqnum
    191          log.info( message )
    192          sendEvent( self, message=message )
    193          transport.SSHClientTransport.receiveUnimplemented(self, seqnum)
    194
    195
    196 -    def receiveDebug( self, alwaysDisplay, message, lang ):
    197          """
    198          Called when a debug message was received from the device.
    199
    200          @param alwaysDisplay: boolean-type code to indicate if the message is to be displayed
    201          @type alwaysDisplay: integer
    202          @param message: debug message from remote device
    203          @type message: string
    204          @param lang: language code
    205          @type lang: integer
    206          """
    207          message= "Debug message from remote device (%s): %s" % ( str(lang), str(message) )
    208          log.info( message )
    209          sendEvent( self, message=message, severity=Event.Debug )
    210
    211          transport.SSHClientTransport.receiveDebug(self, alwaysDisplay, message, lang )
    212
    213
    214 -    def connectionSecure(self):
    215          """
    216          This is called after the connection is set up and other services can be run.
    217          This function starts the SshUserAuth client (ie the Connection client).
    218          """
    219          sshconn = SshConnection(self.factory)
    220          sshauth = SshUserAuth(self.factory.username, sshconn, self.factory)
    221          self.requestService(sshauth)
    222
    223 -class NoPasswordException(Exception):
    224      pass
    225
    226
    227 -class SshUserAuth(userauth.SSHUserAuthClient):
    228      """
    229      Class to gather credentials for use with our SSH connection,
    230      and use them to authenticate against the remote device.
    231      """
    232
    233 -    def __init__(self, user, instance, factory):
    234          """
    235          If no username is supplied, defaults to the user running this code (eg zenoss)
    236
    237          @param user: username
    238          @type user: string
    239          @param instance: instance object
    240          @type instance: object
    241          @param factory: factory info
    242          @type factory: Twisted factory object
    243          """
    244
    245          user = str(user)                # damn unicode
    246          if user == '':
    247              log.debug("Unable to determine username/password from " + \
    248                         "zCommandUser/zCommandPassword")
    249
    250              # From the Python docs about the preferred method of
    251              # obtaining user name in preference to os.getlogin()
    252              #  (http://docs.python.org/library/os.html)
    253              import pwd
    254              try:
    255                  user = os.environ.get( 'LOGNAME', pwd.getpwuid(os.getuid())[0] )
    256              except:
    257                  pass
    258
    259              if user == '':
    260                  message= "No zProperties defined and unable to determine current user."
    261                  log.error( message )
    262                  sendEvent( self, message=message )
    263                  raise SshClientError( message )
    264
    265          userauth.SSHUserAuthClient.__init__(self, user, instance)
    266          self.user = user
    267          self.factory = factory
    268          self._key = self._getKey()
    269
    270
    271 -    def getPassword(self, unused=None):
    272          """
    273          Called from conch.
    274
    275          Return a deferred object of success if there's a password or
    276          return fail (ie no zCommandPassword specified)
    277
    278          @param unused: unused (unused)
    279          @type unused: string
    280          @return: Twisted deferred object (defer.succeed or defer.fail)
    281          @rtype: Twisted deferred object
    282          """
    283          try:
    284              password = self._getPassword()
    285              d = defer.succeed(password)
    286          except NoPasswordException, e:
    287              d = self._handleFailure(str(e))
    288          return d
    289
    290 -    def getGenericAnswers(self, name, instruction, prompts):
    291          """
    292          Called from conch.
    293
    294          Returns a L{Deferred} with the responses to the prompts.
    295
    296          @param name: The name of the authentication currently in progress.
    297          @param instruction: Describes what the authentication wants.
    298          @param prompts: A list of (prompt, echo) pairs, where prompt is a
    299          string to display and echo is a boolean indicating whether the
    300          user's response should be echoed as they type it.
    301          """
    302          log.debug('getGenericAnswers name:"%s" instruction:"%s" prompts:%s',
    303                  name, instruction, pformat(prompts))
    304          if prompts == []:
    305              # RFC 4256 - In the case that the server sends a `0' num-prompts
    306              # field in the request message, the client MUST send a response
    307              # message with a `0' num-responses field to complete the exchange.
    308              d = defer.succeed([])
    309          else:
    310              for prompt, echo in prompts:
    311                  if 'password' in prompt.lower():
    312                      try:
    313                          password = self._getPassword()
    314                          d = defer.succeed([password])
    315                      except NoPasswordException, e:
    316                          d = self._handleFailure(str(e))
    317                      break
    318              else:
    319                  message = 'No known prompts: %s' % pformat(prompts)
    320                  d = self._handleFailure(message)
    321          return d
    322
    323 -    def _getPassword(self):
    324          """
    325          Get the password. Raise an exception if it is not set.
    326          """
    327          if not self.factory.password:
    328              message= "SshUserAuth: no password found -- " + \
    329                       "has zCommandPassword been set?"
    330              raise NoPasswordException(message)
    331          return self.factory.password
    332
    333 -    def _handleFailure(self, message):
    334          """
    335          Handle a failure by logging a message, sending an event, calling
    336          clientFinished, and returning a failure defered.
    337          """
    338          log.error( message )
    339          sendEvent( self, message=message )
    340          self.factory.clientFinished()
    341          return defer.fail( SshClientError( message ) )
    342
    343 -    def _getKey(self):
    344          keyPath = os.path.expanduser(self.factory.keyPath)
    345          log.debug('Expanded SSH key path from zKeyPath %s to %s' % (
    346                  self.factory.keyPath, keyPath))
    347          key = None
    348          if os.path.exists(keyPath):
    349              try:
    350                  data = ''.join(open(keyPath).readlines()).strip()
    351                  key = Key.fromString(data,
    352                                 passphrase=self.factory.password)
    353              except IOError, ex:
    354                  message = "Unable to read the SSH key file because %s" % (
    355                               str(ex))
    356                  log.warn(message)
    357                  device = 'localhost' # Fallback
    358                  try:
    359                      device = socket.getfqdn()
    360                  except:
    361                      pass
    362                  sendEvent(self, device=device, message=message,
    363                            severity=Event.Warning)
    364          else:
    365              log.debug( "SSH key path %s doesn't exist" % keyPath )
    366          return key
    367
    368 -    def getPublicKey(self):
    369          """
    370          Return the SSH public key (using the zProperty zKeyPath) or None
    371
    372          @return: SSH public key
    373          @rtype: string
    374          """
    375          if self._key is not None:
    376              return self._key.blob()
    377
    378 -    def getPrivateKey(self):
    379          """
    380          Return a deferred with the SSH private key (using the zProperty zKeyPath)
    381
    382          @return: Twisted deferred object (defer.succeed)
    383          @rtype: Twisted deferred object
    384          """
    385          if self._key is None:
    386              keyObject = None
    387          else:
    388              keyObject = self._key.keyObject
    389          return defer.succeed(keyObject)
    390
    391 -    def ssh_USERAUTH_FAILURE( self, packet):
    392          """
    393          Called when the SSH session can't authenticate.
    394          NB: This function is also called as an initializer
    395              to start the connections.
    396
    397          @param packet: returned packet from the host
    398          @type packet: object
    399          """
    400          from twisted.conch.ssh.common import getNS
    401          canContinue, partial = getNS(packet)
    402          canContinue = canContinue.split(',')
    403
    404          from Products.ZenUtils.Utils import unused
    405          unused(partial)
    406
    407          lastAuth= getattr( self, "lastAuth", '')
    408          if lastAuth == '' or lastAuth == 'none':
    409              pass   # Start our connection
    410
    411          elif lastAuth == 'publickey':
    412              self.authenticatedWith.append(self.lastAuth)
    413              message= "SSH login to %s with SSH keys failed" % \
    414                        self.factory.hostname
    415              log.error( message )
    416              sendEvent( self, message=message )
    417
    418          elif lastAuth == 'password':
    419              message= "SSH login to %s with username %s failed" % \
    420                       ( self.factory.hostname, self.user )
    421              log.error( message )
    422              sendEvent( self, message=message )
    423
    424              self.factory.loginTries -= 1
    425              log.debug( "Decremented loginTries count to %d" % self.factory.loginTries )
    426
    427              if self.factory.loginTries <= 0:
    428                  message= "SSH connection aborted after maximum login attempts."
    429                  log.error( message )
    430                  sendEvent( self, message=message )
    431
    432              else:
    433                  return self.tryAuth('password')
    434
    435
    436              self.authenticatedWith.append(self.lastAuth)
    437
    438          # Straight from the nasty Twisted code
    439          def _(x, y):
    440              try:
    441                  i1 = self.preferredOrder.index(x)
    442              except ValueError:
    443                  return 1
    444              try:
    445                  i2 = self.preferredOrder.index(y)
    446              except ValueError:
    447                  return -1
    448              return cmp(i1, i2)
    449
    450          canContinue.sort(_)
    451          log.debug( 'Sorted list of authentication methods: %s' % canContinue)
    452          for method in canContinue:
    453              if method not in self.authenticatedWith:
    454                  if self._key is None and method == 'publickey':
    455                      # Attempting a publickey authentication with a blank key
    456                      # causes timeouts that would be hard to track down.
    457                      log.debug("Skipping %s method as the key was blank",
    458                                method )
    459                      self.authenticatedWith.append(method)
    460                      continue
    461
    462                  log.debug( "Attempting method %s" % method )
    463                  if self.tryAuth(method):
    464                      return
    465
    466          log.debug( "All authentication methods attempted" )
    467          self.factory.clientFinished()
    468          self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, 'No more authentication methods available')
    469
    470 -class SshConnection(connection.SSHConnection):
    471      """
    472      Wrapper class that starts channels on top of connections.
    473      """
    474
    475 -    def __init__(self, factory):
    476          """
    477          Initializer
    478
    479          @param factory: factory containing the connection info
    480          @type factory: Twisted factory object
    481          """
    482          log.debug("Creating new SSH connection...")
    483          connection.SSHConnection.__init__(self)
    484          self.factory = factory
    485
    486
    487 -    def ssh_CHANNEL_FAILURE( self, packet):
    488          """
    489          Called when the SSH session can't authenticate
    490
    491          @param packet: returned packet from the host
    492          @type packet: object
    493          """
    494          message= "CHANNEL_FAILURE: Authentication failure"
    495          log.error( message )
    496          sendEvent( self, message=message )
    497          connection.SSHConnection.ssh_CHANNEL_FAILURE(self, packet)
    498
    499
    500 -    def ssh_CHANNEL_OPEN_FAILURE( self, packet):
    501          """
    502          Called when the SSH session can't authenticate
    503
    504          @param packet: returned packet from the host
    505          @type packet: object
    506          """
    507          message= "CHANNEL_OPEN_FAILURE: Try lowering zSshConcurrentSessions"
    508          log.error( message )
    509          sendEvent( self, message=message )
    510          connection.SSHConnection.ssh_CHANNEL_OPEN_FAILURE( self, packet )
    511
    512
    513 -    def ssh_REQUEST_FAILURE( self, packet):
    514          """
    515          Called when the SSH session can't authenticate
    516
    517          @param packet: returned packet from the host
    518          """
    519          message= "REQUEST_FAILURE: Authentication failure"
    520          log.error( message )
    521          sendEvent( self, message=message )
    522          connection.SSHConnection.ssh_REQUEST_FAILURE( self, packet )
    523
    524
    525 -    def openFailed(self, reason):
    526          """
    527          Called when the connection open() fails.
    528          Usually this gets called after too many bad connection attempts,
    529          and the remote device gets upset with us.
    530
    531          NB: reason.desc is the human-readable description of the failure
    532              reason.code is the SSH error code
    533           (see http://tools.ietf.org/html/rfc4250#section-4.2.2 for more details)
    534
    535          @param reason: reason object
    536          @type reason: reason object
    537          """
    538          message= 'SSH connection to %s failed (error code %d): %s' % \
    539                   (self.command, reason.code, str(reason.desc) )
    540          log.error( message )
    541          sendEvent( self, message=message )
    542          connection.SSHConnection.openFailed( self, reason )
    543
    544
    545 -    def serviceStarted(self):
    546          """
    547          Called when the service is active on the transport
    548          """
    549          self.factory.serviceStarted(self)
    550
    551
    552 -    def addCommand(self, cmd):
    553          """
    554          Open a new channel for each command in queue
    555
    556          @param cmd: command to run
    557          @type cmd: string
    558          """
    559          ch = CommandChannel(cmd, conn=self)
    560          self.openChannel(ch)
    561          targetIp = self.transport.transport.addr[0]
    562          log.debug("%s channel %s SshConnection added command %s",
    563                    targetIp, ch.id, cmd)
    564
    565
    566 -    def channelClosed(self, channel):
    567          """
    568          Called when a channel is closed.
    569          REQUIRED function by Twisted.
    570
    571          @param channel: channel that closed
    572          @type channel: Twisted channel object
    573          """
    574          targetIp = self.transport.transport.addr[0]
    575          log.debug("%s channel %s SshConnection closing",
    576                    targetIp, channel.id)
    577          # grr.. patch SSH inherited method to deal with partially
    578          # configured channels
    579          self.localToRemoteChannel[channel.id] = None
    580          self.channelsToRemoteChannel[channel] = None
    581          connection.SSHConnection.channelClosed(self, channel)
    582
    583
    584
    585 -class CommandChannel(channel.SSHChannel):
    586      """
    587      The class that actually interfaces between Zenoss and the device.
    588      """
    589      name = 'session'
    590      conn = None
    591
    592 -    def __init__(self, command, conn=None):
    593          """
    594          Initializer
    595
    596          @param command: command to run
    597          @type command: string
    598          @param conn: connection to create the channel on
    599          @type conn: Twisted connection object
    600          """
    601          channel.SSHChannel.__init__(self, conn=conn)
    602          self.command = command
    603          self.exitCode = None
    604
    605      @property
    606 -    def targetIp(self):
    607          if self.conn:
    608              return self.conn.transport.transport.addr[0]
    609
    610 -    def openFailed(self, reason):
    611          """
    612          Called when the open fails.
    613          """
    614          from twisted.conch.error import ConchError
    615          if isinstance(reason, ConchError):
    616              args = (reason.data, reason.value)
    617          else:
    618              args = (reason.code, reason.desc)
    619          message = 'CommandChannel Open of %s failed (error code %d): %s' % (
    620                  (self.command,) + args)
    621          log.warn("%s %s", self.targetIp, message)
    622          sendEvent(self, message=message)
    623          channel.SSHChannel.openFailed(self, reason)
    624          if self.conn is not None:
    625              self.conn.factory.clientFinished()
    626
    627
    628 -    def extReceived(self, dataType, data ):
    629          """
    630          Called when we receive extended data (usually standard error)
    631
    632          @param dataType: data type code
    633          @type dataType: integer
    634          """
    635          message= 'The command %s returned stderr data (%d) from the device: %s' \
    636                   % (self.command, dataType, data)
    637          log.warn("%s channel %s %s", self.targetIp, self.conn.localChannelID,
    638                   message)
    639          sendEvent(self, message=message)
    640
    641
    642 -    def channelOpen(self, unused):
    643          """
    644          Initialize the channel and send our command to the device.
    645
    646          @param unused: unused (unused)
    647          @type unused: string
    648          @return: Twisted channel
    649          @rtype: Twisted channel
    650          """
    651
    652          log.debug('%s channel %s Opening command channel for %s',
    653                    self.targetIp, self.conn.localChannelID, self.command)
    654          self.data = ''
    655
    656          #  Notes for sendRequest:
    657          # 'exec'      - execute the following command and exit
    658          # common.NS() - encodes the command as a length-prefixed string
    659          # wantReply   - reply to let us know the process has been started
    660          d = self.conn.sendRequest(self, 'exec', common.NS(self.command),
    661                                    wantReply=1)
    662          return d
    663
    664
    665 -    def request_exit_status(self, data):
    666          """
    667          Gathers the exit code from the device
    668
    669          @param data: returned value from device
    670          @type data: packet
    671          """
    672          import struct
    673          self.exitCode = struct.unpack('>L', data)[0]
    674          log.debug("%s channel %s CommandChannel exit code for %s is %d: %s",
    675                    self.targetIp, getattr(self.conn, 'localChannelID', None),
    676                    self.command, self.exitCode, getExitMessage(self.exitCode))
    677
    678
    679 -    def dataReceived(self, data):
    680          """
    681          Response stream from the device.  Can be called multiple times.
    682
    683          @param data: returned value from device
    684          @type data: string
    685          """
    686          self.data += data
    687
    688
    689 -    def closed(self):
    690          """
    691          Cleanup for the channel, as both ends have closed the channel.
    692          """
    693          log.debug('%s channel %s CommandChannel closing command channel for command %s with data: %s',
    694                    self.targetIp, getattr(self.conn, 'localChannelID', None),
    695                    self.command, repr(self.data))
    696          self.conn.factory.addResult(self.command, self.data, self.exitCode)
    697          self.loseConnection()
    698
    699          self.conn.factory.channelClosed()
    700
    701
    702
    703 -class SshClient(CollectorClient.CollectorClient):
    704      """
    705      SSH Collector class to connect to a particular device
    706      """
    707
    708 -    def __init__(self, hostname, ip, port=22, plugins=[], options=None,
    709                      device=None, datacollector=None, isLoseConnection=False):
    710          """
    711          Initializer
    712
    713          @param hostname: hostname of the device
    714          @type hostname: string
    715          @param ip: IP address of the device
    716          @type ip: string
    717          @param port: port number to use to connect to device
    718          @type port: integer
    719          @param plugins: plugins
    720          @type plugins: list of plugins
    721          @param options: options
    722          @type options: list
    723          @param device: name of device
    724          @type device: string
    725          @param datacollector: object
    726          @type datacollector: object
    727          """
    728
    729          CollectorClient.CollectorClient.__init__(self, hostname, ip, port,
    730                             plugins, options, device, datacollector)
    731          self.hostname = hostname
    732          self.protocol = SshClientTransport
    733          self.connection = None
    734          self.transport = None
    735          self.openSessions = 0
    736          self.workList = list(self.getCommands())
    737          self.isLoseConnection = isLoseConnection
    738
    739 -    def run(self):
    740          """
    741          Start SSH collection.
    742          """
    743          log.debug("%s SshClient connecting to %s:%s with timeout %s seconds",
    744                         self.ip, self.hostname, self.port, self.loginTimeout)
    745          reactor.connectTCP(self.ip, self.port, self, self.loginTimeout)
    746
    747
    748 -    def runCommands(self):
    749          log.debug("%s SshClient has %d commands to assign to channels (max = %s, current = %s)",
    750                    self.ip, len(self.workList), self.concurrentSessions, self.openSessions)
    751          availSessions = self.concurrentSessions - self.openSessions
    752          for i in range(min(len(self.workList), availSessions)):
    753              cmd = self.workList.pop(0)
    754              self.openSessions += 1
    755              self.connection.addCommand(cmd)
    756
    757
    758 -    def channelClosed(self):
    759          self.openSessions -= 1
    760          log.debug("%s SshClient closing channel (openSessions = %s)",
    761                    self.ip, self.openSessions)
    762          if self.commandsFinished():
    763              if self.isLoseConnection:
    764                  self.transport.loseConnection()
    765              self.clientFinished()
    766              return
    767
    768          if self.workList:
    769              cmd = self.workList.pop(0)
    770              self.openSessions += 1
    771              if self.connection:
    772                  self.connection.addCommand(cmd)
    773
    774
    775 -    def serviceStarted(self, sshconn):
    776          """
    777          Run commands that are in the command queue
    778
    779          @param sshconn: connection to create channels on
    780          @type sshconn: Twisted SSH connection
    781          """
    782          log.debug("SshClient connected to device %s (%s)", self.hostname, self.ip)
    783          self.connection = sshconn
    784          self.runCommands()
    785
    786
    787 -    def addCommand(self, commands):
    788          """
    789          Add a command or commands to queue and open a command
    790          channel for each command
    791
    792          @param commands: commands to run
    793          @type commands: list
    794          """
    795          CollectorClient.CollectorClient.addCommand(self, commands)
    796          if isinstance(commands, basestring):
    797              commands = (commands,)
    798          self.workList.extend(commands)
    799
    800          # This code is required when we're reused by zencommand.
    801          if self.connection:
    802              self.runCommands()
    803
    804
    805 -    def clientConnectionFailed( self, connector, reason ):
    806          """
    807          If we didn't connect let the modeler know
    808
    809          @param connector: connector associated with this failure
    810          @type connector: object
    811          @param reason: failure object
    812          @type reason: object
    813          """
    814          from Products.ZenUtils.Utils import unused
    815          unused(connector)
    816          message= reason.getErrorMessage()
    817          log.error("%s %s", self.ip, message)
    818          sendEvent(self, device=self.hostname, message=message)
    819          self.clientFinished()
    820
    821
    822 -    def loseConnection(self):
    823          """
    824          Called when the connection gets closed.
    825          """
    826          log.debug("%s SshClient connection closed", self.ip)
    827          #self.connection.loseConnection()
    828
    829
    830
    831 -def main():
    832      """
    833      Test harness main()
    834
    835      Usage:
    836
    837      python SshClient.py hostname[:port] comand [command]
    838
    839      Each command must be enclosed in quotes (") to be interpreted
    840      properly as a complete unit.
    841      """
    842      from itertools import chain
    843      import pprint
    844
    845      logging.basicConfig()
    846
    847      parser = CollectorClient.buildOptions()
    848      options = CollectorClient.parseOptions(parser,22)
    849      log.setLevel(options.logseverity)
    850
    851      client = SshClient(options.hostname,
    852                         getHostByName(options.hostname),
    853                         options.port,
    854                         options=options)
    855
    856      # Rather than getting info from zenhub, just pass our
    857      # commands in
    858      client.workList= options.commands
    859
    860      client.run()
    861
    862      client.clientFinished= reactor.stop
    863      client._commands.append( options.commands )
    864      reactor.run()
    865
    866      pprint.pprint(client.getResults())
    867
    868
    869  if __name__ == '__main__':
    870      main()
    871