2020
2121logger = logging .getLogger (__name__ )
2222
23+
2324def _setup_logger (_logger ):
2425 """Setup default logger"""
2526 _handler = logging .StreamHandler ()
@@ -28,21 +29,36 @@ def _setup_logger(_logger):
2829 _logger .addHandler (handler )
2930 _logger .setLevel (logging .DEBUG )
3031
31- class UnknownHostException (Exception ): pass
32- class ConnectionErrorException (Exception ): pass
32+
33+ class UnknownHostException (Exception ):
34+ """Raised when a host is unknown (dns failure)"""
35+ pass
36+
37+
38+ class ConnectionErrorException (Exception ):
39+ """Raised on error connecting (connection refused/timed out)"""
40+ pass
41+
42+
43+ class AuthenticationException (Exception ):
44+ """Raised on authentication error (user/password/ssh key error"""
45+ pass
46+
3347
3448class SSHClient (object ):
3549 """Wrapper class over paramiko.SSHClient with sane defaults"""
3650
3751 def __init__ (self , host ,
38- user = None ,
39- password = None , port = None ):
40- """Connect to host honoring any user set configuration in ~/.ssh/config or /etc/ssh/ssh_config
52+ user = None ,
53+ password = None , port = None ):
54+ """Connect to host honoring any user set configuration in ~/.ssh/config
55+ or /etc/ssh/ssh_config
4156 :type: str
4257 :param host: Hostname to connect to
4358 :type str:
44- :param user: (Optional) User to login as. Defaults to logged in user or user from ~/.ssh/config if set
45- :throws: paramiko.AuthenticationException on authentication error
59+ :param user: (Optional) User to login as. Defaults to logged in user or
60+ user from ~/.ssh/config if set
61+ :throws: ssh_client.AuthenticationException on authentication error
4662 :throws: ssh_client.UnknownHostException on DNS resolution error
4763 :throws: ssh_client.ConnectionErrorException on error connecting"""
4864 ssh_config = paramiko .SSHConfig ()
@@ -75,48 +91,55 @@ def __init__(self, host,
7591 def _connect (self ):
7692 """Connect to host, throw UnknownHost exception on DNS errors"""
7793 try :
78- self .client .connect (self .host , username = self .user , password = self .password , port = self .port )
94+ self .client .connect (self .host , username = self .user ,
95+ password = self .password , port = self .port )
7996 except socket .gaierror , e :
80- logger .error ("Could not resolve host '%s'" % (self .host ,))
81- raise UnknownHostException ("%s - %s" % (str (e .args [1 ]), self .host ,))
97+ logger .error ("Could not resolve host '%s'" , self .host ,)
98+ raise UnknownHostException ("%s - %s" % (str (e .args [1 ]),
99+ self .host ,))
82100 except socket .error , e :
83- logger .error ("Error connecting to host '%s'" % (self .host ,))
84- raise ConnectionErrorException ("%s for host '%s'" % (str (e .args [1 ]), self .host ,))
101+ logger .error ("Error connecting to host '%s'" , self .host ,)
102+ raise ConnectionErrorException ("%s for host '%s'" % (str (e .args [1 ]),
103+ self .host ,))
104+ except paramiko .AuthenticationException , e :
105+ raise AuthenticationException (e )
85106
86- def exec_command (self , command , sudo = False , ** kwargs ):
107+ def exec_command (self , command , sudo = False , ** kwargs ):
87108 """Wrapper to paramiko.SSHClient.exec_command"""
88-
89109 channel = self .client .get_transport ().open_session ()
90110 channel .get_pty ()
91- _ , stdout , stderr = channel .makefile ('wb' ), channel .makefile ('rb' ), channel .makefile_stderr ('rb' )
111+ (_ , stdout , stderr ) = (channel .makefile ('wb' ), channel .makefile ('rb' ),
112+ channel .makefile_stderr ('rb' ))
92113 if sudo :
93114 command = 'sudo -S bash -c "%s"' % command .replace ('"' , '\\ "' )
94- logger .debug ("Running command %s on %s" % ( command , self .host ) )
115+ logger .debug ("Running command %s on %s" , command , self .host )
95116 channel .exec_command (command , ** kwargs )
96117 logger .debug ("Command finished executing" )
97118 while not channel .recv_ready ():
98119 gevent .sleep (.2 )
99120 return channel , self .host , stdout , stderr
100121
122+
101123class ParallelSSHClient (object ):
102124 """Uses SSHClient, runs command on multiple hosts in parallel"""
103125
104126 def __init__ (self , hosts ,
105- user = None , password = None , port = None ,
106- pool_size = 10 ):
127+ user = None , password = None , port = None ,
128+ pool_size = 10 ):
107129 """Connect to hosts
108130 :type: list(str)
109131 :param hosts: Hosts to connect to
110132 :type: int
111133 :param pool_size: Pool size - how many commands to run in parallel
112134 :type str:
113- :param user: (Optional) User to login as. Defaults to logged in user or user from ~/.ssh/config if set
135+ :param user: (Optional) User to login as. Defaults to logged in user or
136+ user from ~/.ssh/config if set
114137 :type str:
115138 :param password: (Optional) Password to use for login
116139 :throws: paramiko.AuthenticationException on authentication error
117140 :throws: ssh_client.UnknownHostException on DNS resolution error
118141 :throws: ssh_client.ConnectionErrorException on error connecting"""
119- self .pool = gevent .pool .Pool (size = pool_size )
142+ self .pool = gevent .pool .Pool (size = pool_size )
120143 self .pool_size = pool_size
121144 self .hosts = hosts
122145 self .user = user
@@ -127,24 +150,27 @@ def __init__(self, hosts,
127150
128151 def exec_command (self , * args , ** kwargs ):
129152 """Run command on all hosts in parallel, honoring self.pool_size"""
130- return [self .pool .spawn (self ._exec_command , host , * args , ** kwargs ) for host in self .hosts ]
153+ return [self .pool .spawn (self ._exec_command , host , * args , ** kwargs )
154+ for host in self .hosts ]
131155
132156 def _exec_command (self , host , * args , ** kwargs ):
133157 """Make SSHClient, run command on host"""
134158 if not self .host_clients [host ]:
135- self .host_clients [host ] = SSHClient (host , user = self .user , password = self .password ,
136- port = self .port )
159+ self .host_clients [host ] = SSHClient (host , user = self .user ,
160+ password = self .password ,
161+ port = self .port )
137162 return self .host_clients [host ].exec_command (* args , ** kwargs )
138163
139164 def get_stdout (self , greenlet ):
140165 """Print stdout from greenlet and return exit code for host"""
141166 channel , host , stdout , stderr = greenlet .get ()
142167 for line in stdout :
143- host_logger .info ("[%s]\t %s" % ( host , line .strip (),) )
168+ host_logger .info ("[%s]\t %s" , host , line .strip (),)
144169 for line in stderr :
145- host_logger .info ("[%s] [err] %s" % ( host , line .strip (),) )
170+ host_logger .info ("[%s] [err] %s" , host , line .strip (),)
146171 channel .close ()
147- return {host : {'exit_code' : channel .recv_exit_status ()}}
172+ return {host : {'exit_code' : channel .recv_exit_status ()}}
173+
148174
149175def test ():
150176 client = SSHClient ('localhost' )
0 commit comments