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

# Source Code for Module Products.DataCollector.SshClient

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