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 as published by
      8  # the Free Software Foundation.
      9  #
     10  # For complete information please visit: http://www.zenoss.com/oss/
     11  #
     12  ###########################################################################
     13
     14  __doc__="""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
     27  import Globals
     28
     29  from twisted.conch.ssh import transport, userauth, connection
     30  from twisted.conch.ssh import common, channel
     31  from twisted.conch.ssh.keys import Key
     32  from twisted.internet import defer, reactor
     33  from Products.ZenEvents import Event
     34  from Products.ZenUtils.Utils import getExitMessage
     35
     36  from Exceptions import *
     37
     38  import CollectorClient
     39
     40  # NB: Most messages returned back from Twisted are Unicode.
     41  #     Expect to use str() to convert to ASCII before dumping out. :)
     42
     43
     44 -def sendEvent( self, message="", device='', severity=Event.Error ):
     45      """
     46      Shortcut version of sendEvent()
     47
     48      @param message: message to send in Zenoss event
     49      @type message: string
     50      @param device: hostname of device to which this event is associated
     51      @type device: string
     52      @param severity: Zenoss severity from Products.ZenEvents
     53      @type severity: integer
     54      """
     55
     56      # Parse out the daemon's name
     57      component= os.path.basename( sys.argv[0] ).replace( '.py', '' )
     58
     59      def hasattr_path( object_root, path ):
     60          """
     61          The regular hasattr() only works on one component,
     62          not multiples.
     63
     64          @param object_root: object to start searching for path
     65          @type object_root: object
     66          @param path: path to func or variable (eg "conn.factory" )
     67          @type path: string
     68          @return: is object_root.path sane?
     69          @rtype: boolean
     70          """
     71          obj = object_root
     72          for chunk in path.split('.'):
     73              obj= getattr( obj, chunk, None )
     74              if obj is None:
     75                  return False
     76          return True
     77
     78      # ... and the device's name (as known by Zenoss)
     79      if device == '':
     80         if hasattr_path( self, "factory.hostname" ):
     81             device= self.factory.hostname
     82
     83         elif hasattr_path( self, "conn.factory.hostname" ):
     84             device= self.conn.factory.hostname
     85
     86         else:
     87             log.debug( "Couldn't get the remote device's hostname" )
     88
     89      error_event= {
     90         'agent': component,
     91         'summary': message,
     92         'device': device,
     93         'eventClass': "/Cmd/Fail",
     94         'component': component,
     95         'severity': severity,
     96      }
     97
     98      # At this point, we don't know what we have
     99      try:
    100          if hasattr_path( self, "factory.datacollector.sendEvent" ):
    101              self.factory.datacollector.sendEvent( error_event )
    102
    103          elif hasattr_path( self, "factory.sendEvent" ):
    104              self.factory.sendEvent( error_event )
    105
    106          elif hasattr_path( self, "datacollector.sendEvent" ):
    107              self.datacollector.sendEvent( error_event )
    108
    109          elif hasattr_path( self, "conn.factory.datacollector.sendEvent" ):
    110              self.conn.factory.datacollector.sendEvent( error_event )
    111
    112          else:
    113              log.debug( "Unable to send event for %s" % error_event )
    114
    115      except:
    116          pass # Don't cause other issues
    117
    118
    119
    120 -class SshClientError( Exception ):
    121      """
    122      Exception class
    123      """
    124
    125
    126
    127 -class SshClientTransport(transport.SSHClientTransport):
    128      """
    129      Base client class for constructing Twisted Conch services.
    130      This class is *only* responsible for connecting to the SSH
    131      service on the device, and ensuring that *host* keys are sane.
    132      """
    133
    134 -    def verifyHostKey(self, hostKey, fingerprint):
    135          """
    136          Module to verify the host's SSH key against the stored fingerprint we have
    137          from the last time that we communicated with the host.
    138
    139          NB: currently does not verify this information but simply trusts every host key
    140
    141          @param hostKey: host's SSH key (unused)
    142          @type hostKey: string
    143          @param fingerprint: host fingerprint (unused)
    144          @type fingerprint: string
    145          @return: Twisted deferred object
    146          @rtype: Twisted deferred object (defer.succeed(1)
    147          @todo: verify the host key
    148          """
    149          #blowing off host key right now, should store and check
    150          from Products.ZenUtils.Utils import unused
    151          unused(hostKey)
    152          log.debug('%s host fingerprint: %s' % (self.factory.hostname, fingerprint))
    153          return defer.succeed(1)
    154
    155
    156 -    def connectionMade(self):
    157          """
    158          Called after the connection has been made.
    159          Used to set up private instance variables.
    160          """
    161          self.factory.transport = self.transport
    162          transport.SSHClientTransport.connectionMade(self)
    163
    164
    165 -    def receiveError( self, reasonCode, description ):
    166          """
    167          Called when a disconnect error message was received from the device.
    168
    169          @param reasonCode: error code from SSH connection failure
    170          @type reasonCode: integer
    171          @param description: human-readable version of the error code
    172          @type description: string
    173          """
    174          message= 'SSH error from remote device (code %d): %s\n' % \
    175                   ( reasonCode, str( description ) )
    176          log.warn( message )
    177          sendEvent( self, message=message )
    178          transport.SSHClientTransport.receiveError(self, reasonCode, description )
    179
    180
    181 -    def receiveUnimplemented( self, seqnum ):
    182          """
    183          Called when an unimplemented packet message was received from the device.
    184
    185          @param seqnum: SSH message code
    186          @type seqnum: integer
    187          """
    188          message= "Got 'unimplemented' SSH message, seqnum= %d" % seqnum
    189          log.info( message )
    190          sendEvent( self, message=message )
    191          transport.SSHClientTransport.receiveUnimplemented(self, seqnum)
    192
    193
    194 -    def receiveDebug( self, alwaysDisplay, message, lang ):
    195          """
    196          Called when a debug message was received from the device.
    197
    198          @param alwaysDisplay: boolean-type code to indicate if the message is to be displayed
    199          @type alwaysDisplay: integer
    200          @param message: debug message from remote device
    201          @type message: string
    202          @param lang: language code
    203          @type lang: integer
    204          """
    205          message= "Debug message from remote device (%s): %s" % ( str(lang), str(message) )
    206          log.info( message )
    207          sendEvent( self, message=message, severity=Event.Debug )
    208
    209          transport.SSHClientTransport.receiveDebug(self, alwaysDisplay, message, lang )
    210
    211
    212 -    def connectionSecure(self):
    213          """
    214          This is called after the connection is set up and other services can be run.
    215          This function starts the SshUserAuth client (ie the Connection client).
    216          """
    217          sshconn = SshConnection(self.factory)
    218          sshauth = SshUserAuth(self.factory.username, sshconn, self.factory)
    219          self.requestService(sshauth)
    220
    221 -class NoPasswordException(Exception):
    222      pass
    223
    224
    225 -class SshUserAuth(userauth.SSHUserAuthClient):
    226      """
    227      Class to gather credentials for use with our SSH connection,
    228      and use them to authenticate against the remote device.
    229      """
    230
    231 -    def __init__(self, user, instance, factory):
    232          """
    233          If no username is supplied, defaults to the user running this code (eg zenoss)
    234
    235          @param user: username
    236          @type user: string
    237          @param instance: instance object
    238          @type instance: object
    239          @param factory: factory info
    240          @type factory: Twisted factory object
    241          """
    242
    243          user = str(user)                # damn unicode
    244          if user == '':
    245              log.debug("Unable to determine username/password from " + \
    246                         "zCommandUser/zCommandPassword")
    247
    248              # From the Python docs about the preferred method of
    249              # obtaining user name in preference to os.getlogin()
    250              #  (http://docs.python.org/library/os.html)
    251              import pwd
    252              try:
    253                  user = os.environ.get( 'LOGNAME', pwd.getpwuid(os.getuid())[0] )
    254              except:
    255                  pass
    256
    257              if user == '':
    258                  message= "No zProperties defined and unable to determine current user."
    259                  log.error( message )
    260                  sendEvent( self, message=message )
    261                  raise SshClientError( message )
    262
    263          userauth.SSHUserAuthClient.__init__(self, user, instance)
    264          self.user = user
    265          self.factory = factory
    266          self._key = self._getKey()
    267
    268
    269 -    def getPassword(self, unused=None):
    270          """
    271          Called from conch.
    272
    273          Return a deferred object of success if there's a password or
    274          return fail (ie no zCommandPassword specified)
    275
    276          @param unused: unused (unused)
    277          @type unused: string
    278          @return: Twisted deferred object (defer.succeed or defer.fail)
    279          @rtype: Twisted deferred object
    280          """
    281          try:
    282              password = self._getPassword()
    283              d = defer.succeed(password)
    284          except NoPasswordException, e:
    285              d = self._handleFailure(str(e))
    286          return d
    287
    288 -    def getGenericAnswers(self, name, instruction, prompts):
    289          """
    290          Called from conch.
    291
    292          Returns a L{Deferred} with the responses to the prompts.
    293
    294          @param name: The name of the authentication currently in progress.
    295          @param instruction: Describes what the authentication wants.
    296          @param prompts: A list of (prompt, echo) pairs, where prompt is a
    297          string to display and echo is a boolean indicating whether the
    298          user's response should be echoed as they type it.
    299          """
    300          log.debug('getGenericAnswers name:"%s" instruction:"%s" prompts:%s',
    301                  name, instruction, pformat(prompts))
    302          if prompts == []:
    303              # RFC 4256 - In the case that the server sends a `0' num-prompts
    304              # field in the request message, the client MUST send a response
    305              # message with a `0' num-responses field to complete the exchange.
    306              d = defer.succeed([])
    307          else:
    308              for prompt, echo in prompts:
    309                  if 'password' in prompt.lower():
    310                      try:
    311                          password = self._getPassword()
    312                          d = defer.succeed([password])
    313                      except NoPasswordException, e:
    314                          d = self._handleFailure(str(e))
    315                      break
    316              else:
    317                  message = 'No known prompts: %s' % pformat(prompts)
    318                  d = self._handleFailure(message)
    319          return d
    320
    321 -    def _getPassword(self):
    322          """
    323          Get the password. Raise an exception if it is not set.
    324          """
    325          if not self.factory.password:
    326              message= "SshUserAuth: no password found -- " + \
    327                       "has zCommandPassword been set?"
    328              raise NoPasswordException(message)
    329          return self.factory.password
    330
    331 -    def _handleFailure(self, message):
    332          """
    333          Handle a failure by logging a message, sending an event, calling
    334          clientFinished, and returning a failure defered.
    335          """
    336          log.error( message )
    337          sendEvent( self, message=message )
    338          self.factory.clientFinished()
    339          return defer.fail( SshClientError( message ) )
    340
    341 -    def _getKey(self):
    342          keyPath = os.path.expanduser(self.factory.keyPath)
    343          log.debug('Expanded SSH key path from zKeyPath %s to %s' % (
    344                  self.factory.keyPath, keyPath))
    345          if os.path.exists(keyPath):
    346              data = ''.join(open(keyPath).readlines()).strip()
    347              key = Key.fromString(data,
    348                                 passphrase=self.factory.password)
    349          else:
    350              key = None
    351              log.debug( "SSH key path %s doesn't exist" % keyPath )
    352          return key
    353
    354 -    def getPublicKey(self):
    355          """
    356          Return the SSH public key (using the zProperty zKeyPath) or None
    357
    358          @return: SSH public key
    359          @rtype: string
    360          """
    361          if self._key is not None:
    362              return self._key.blob()
    363
    364 -    def getPrivateKey(self):
    365          """
    366          Return a deferred with the SSH private key (using the zProperty zKeyPath)
    367
    368          @return: Twisted deferred object (defer.succeed)
    369          @rtype: Twisted deferred object
    370          """
    371          if self._key is None:
    372              keyObject = None
    373          else:
    374              keyObject = self._key.keyObject
    375          return defer.succeed(keyObject)
    376
    377 -    def ssh_USERAUTH_FAILURE( self, packet):
    378          """
    379          Called when the SSH session can't authenticate.
    380          NB: This function is also called as an initializer
    381              to start the connections.
    382
    383          @param packet: returned packet from the host
    384          @type packet: object
    385          """
    386          from twisted.conch.ssh.common import getNS
    387          canContinue, partial = getNS(packet)
    388          canContinue = canContinue.split(',')
    389
    390          from Products.ZenUtils.Utils import unused
    391          unused(partial)
    392
    393          lastAuth= getattr( self, "lastAuth", '')
    394          if lastAuth == '' or lastAuth == 'none':
    395              pass   # Start our connection
    396
    397          elif lastAuth == 'publickey':
    398              self.authenticatedWith.append(self.lastAuth)
    399              message= "SSH login to %s with SSH keys failed" % \
    400                        self.factory.hostname
    401              log.error( message )
    402              sendEvent( self, message=message )
    403
    404          elif lastAuth == 'password':
    405              message= "SSH login to %s with username %s failed" % \
    406                       ( self.factory.hostname, self.user )
    407              log.error( message )
    408              sendEvent( self, message=message )
    409
    410              self.factory.loginTries -= 1
    411              log.debug( "Decremented loginTries count to %d" % self.factory.loginTries )
    412
    413              if self.factory.loginTries <= 0:
    414                  message= "SSH connection aborted after maximum login attempts."
    415                  log.error( message )
    416                  sendEvent( self, message=message )
    417
    418              else:
    419                  return self.tryAuth('password')
    420
    421
    422              self.authenticatedWith.append(self.lastAuth)
    423
    424          # Straight from the nasty Twisted code
    425          def _(x, y):
    426              try:
    427                  i1 = self.preferredOrder.index(x)
    428              except ValueError:
    429                  return 1
    430              try:
    431                  i2 = self.preferredOrder.index(y)
    432              except ValueError:
    433                  return -1
    434              return cmp(i1, i2)
    435
    436          canContinue.sort(_)
    437          log.debug( 'Sorted list of authentication methods: %s' % canContinue)
    438          for method in canContinue:
    439              if method not in self.authenticatedWith:
    440                  log.debug( "Attempting method %s" % method )
    441                  if self.tryAuth(method):
    442                      return
    443
    444          log.debug( "All authentication methods attempted" )
    445          self.factory.clientFinished()
    446          self.transport.sendDisconnect(transport.DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE, 'No more authentication methods available')
    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              self.connection.addCommand(cmd)
    750
    751
    752 -    def serviceStarted(self, sshconn):
    753          """
    754          Run commands that are in the command queue
    755
    756          @param sshconn: connection to create channels on
    757          @type sshconn: Twisted SSH connection
    758          """
    759
    760          log.info("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 type(commands) == type(''):
    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      import socket
    821      from itertools import chain
    822      import pprint
    823
    824      logging.basicConfig()
    825
    826      parser = CollectorClient.buildOptions()
    827      options = CollectorClient.parseOptions(parser,22)
    828      log.setLevel(options.logseverity)
    829
    830      client = SshClient(options.hostname,
    831                         socket.gethostbyname(options.hostname),
    832                         options.port,
    833                         options=options)
    834
    835      # Rather than getting info from zenhub, just pass our
    836      # commands in
    837      client.workList= options.commands
    838
    839      client.run()
    840
    841      client.clientFinished= reactor.stop
    842      client._commands.append( options.commands )
    843      reactor.run()
    844
    845      pprint.pprint(client.getResults())
    846
    847
    848  if __name__ == '__main__':
    849      main()
    850