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

Source Code for Module Products.DataCollector.SshClient

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