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