16
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
18
18
import logging
19
+ from gevent import sleep
19
20
20
21
from .base_pssh import BaseParallelSSHClient
21
22
from .constants import DEFAULT_RETRIES , RETRY_DELAY
22
23
from .ssh2_client import SSHClient
24
+ from .exceptions import ProxyError
25
+ from .tunnel import Tunnel
23
26
24
27
25
28
logger = logging .getLogger (__name__ )
@@ -30,7 +33,9 @@ class ParallelSSHClient(BaseParallelSSHClient):
30
33
31
34
def __init__ (self , hosts , user = None , password = None , port = None , pkey = None ,
32
35
num_retries = DEFAULT_RETRIES , timeout = None , pool_size = 10 ,
33
- allow_agent = True , host_config = None , retry_delay = RETRY_DELAY ):
36
+ allow_agent = True , host_config = None , retry_delay = RETRY_DELAY ,
37
+ proxy_host = None , proxy_port = 22 ,
38
+ proxy_user = None , proxy_password = None , proxy_pkey = None ):
34
39
"""
35
40
:param hosts: Hosts to connect to
36
41
:type hosts: list(str)
@@ -64,23 +69,47 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
64
69
not all hosts use the same configuration.
65
70
:type host_config: dict
66
71
:param allow_agent: (Optional) set to False to disable connecting to
67
- the system's SSH agent
72
+ the system's SSH agent.
68
73
:type allow_agent: bool
74
+ :param proxy_host: (Optional) SSH host to tunnel connection through
75
+ so that SSH clients connect to host via client -> proxy_host -> host
76
+ :type proxy_host: str
77
+ :param proxy_port: (Optional) SSH port to use to login to proxy host if
78
+ set. Defaults to 22.
79
+ :type proxy_port: int
80
+ :param proxy_user: (Optional) User to login to ``proxy_host`` as.
81
+ Defaults to logged in user.
82
+ :type proxy_user: str
83
+ :param proxy_password: (Optional) Password to login to ``proxy_host``
84
+ with. Defaults to no password.
85
+ :type proxy_password: str
86
+ :param proxy_pkey: (Optional) Private key file to be used for
87
+ authentication with ``proxy_host``. Defaults to available keys from
88
+ SSHAgent and user's SSH identities.
89
+ :type proxy_pkey: Private key file path to use. Note that the public
90
+ key file pair *must* also exist in the same location with name
91
+ ``<pkey>.pub``.
69
92
"""
70
93
BaseParallelSSHClient .__init__ (
71
94
self , hosts , user = user , password = password , port = port , pkey = pkey ,
72
95
allow_agent = allow_agent , num_retries = num_retries ,
73
96
timeout = timeout , pool_size = pool_size ,
74
97
host_config = host_config , retry_delay = retry_delay )
98
+ self .proxy_host = proxy_host
99
+ self .proxy_port = proxy_port
100
+ self .proxy_pkey = proxy_pkey
101
+ self .proxy_user = proxy_user
102
+ self .proxy_password = proxy_password
103
+ self ._tunnels = {}
75
104
76
105
def run_command (self , command , sudo = False , user = None , stop_on_errors = True ,
77
106
use_pty = False , host_args = None , shell = None ,
78
107
encoding = 'utf-8' ):
79
108
"""Run command on all hosts in parallel, honoring self.pool_size,
80
109
and return output dictionary.
81
110
82
- This function will block until all commands have been successfully
83
- received by remote servers and then return immediately.
111
+ This function will block until all commands have been received
112
+ by remote servers and then return immediately.
84
113
85
114
More explicitly, function will return after connection and
86
115
authentication establishment and after commands have been accepted by
@@ -139,7 +168,10 @@ def run_command(self, command, sudo=False, user=None, stop_on_errors=True,
139
168
:raises: :py:class:`TypeError` on not enough host arguments for cmd
140
169
string format
141
170
:raises: :py:class:`KeyError` on no host argument key in arguments
142
- dict for cmd string format"""
171
+ dict for cmd string format
172
+ :raises: :py:class:`pssh.exceptions.ProxyErrors` on errors connecting
173
+ to proxy if a proxy host has been set.
174
+ """
143
175
return BaseParallelSSHClient .run_command (
144
176
self , command , stop_on_errors = stop_on_errors , host_args = host_args ,
145
177
user = user , shell = shell , sudo = sudo ,
@@ -184,11 +216,34 @@ def _get_exit_code(self, channel):
184
216
return
185
217
return channel .get_exit_status ()
186
218
219
+ def _start_tunnel (self , host ):
220
+ tunnel = Tunnel (
221
+ self .proxy_host , host , self .port , user = self .proxy_user ,
222
+ password = self .proxy_password , port = self .proxy_port ,
223
+ pkey = self .proxy_pkey , num_retries = self .num_retries ,
224
+ timeout = self .timeout , retry_delay = self .retry_delay ,
225
+ allow_agent = self .allow_agent )
226
+ tunnel .daemon = True
227
+ tunnel .start ()
228
+ self ._tunnels [host ] = tunnel
229
+ while not tunnel .tunnel_open .is_set ():
230
+ logger .debug ("Waiting for tunnel to become active" )
231
+ sleep (.1 )
232
+ if not tunnel .is_alive ():
233
+ msg = "Proxy authentication failed"
234
+ logger .error (msg )
235
+ raise ProxyError (msg )
236
+ return tunnel
237
+
187
238
def _make_ssh_client (self , host ):
188
239
if host not in self .host_clients or self .host_clients [host ] is None :
240
+ if self .proxy_host is not None :
241
+ tunnel = self ._start_tunnel (host )
189
242
_user , _port , _password , _pkey = self ._get_host_config_values (host )
243
+ _host = host if self .proxy_host is None else '127.0.0.1'
244
+ _port = _port if self .proxy_host is None else tunnel .listen_port
190
245
self .host_clients [host ] = SSHClient (
191
- host , user = _user , password = _password , port = _port , pkey = _pkey ,
246
+ _host , user = _user , password = _password , port = _port , pkey = _pkey ,
192
247
num_retries = self .num_retries , timeout = self .timeout ,
193
248
allow_agent = self .allow_agent , retry_delay = self .retry_delay )
194
249
0 commit comments