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