1616# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1717
1818import logging
19+ from gevent import sleep
1920
2021from .base_pssh import BaseParallelSSHClient
2122from .constants import DEFAULT_RETRIES , RETRY_DELAY
2223from .ssh2_client import SSHClient
24+ from .exceptions import ProxyError
25+ from .tunnel import Tunnel
2326
2427
2528logger = logging .getLogger (__name__ )
@@ -30,7 +33,9 @@ class ParallelSSHClient(BaseParallelSSHClient):
3033
3134 def __init__ (self , hosts , user = None , password = None , port = None , pkey = None ,
3235 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 ):
3439 """
3540 :param hosts: Hosts to connect to
3641 :type hosts: list(str)
@@ -64,23 +69,47 @@ def __init__(self, hosts, user=None, password=None, port=None, pkey=None,
6469 not all hosts use the same configuration.
6570 :type host_config: dict
6671 :param allow_agent: (Optional) set to False to disable connecting to
67- the system's SSH agent
72+ the system's SSH agent.
6873 :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``.
6992 """
7093 BaseParallelSSHClient .__init__ (
7194 self , hosts , user = user , password = password , port = port , pkey = pkey ,
7295 allow_agent = allow_agent , num_retries = num_retries ,
7396 timeout = timeout , pool_size = pool_size ,
7497 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 = {}
75104
76105 def run_command (self , command , sudo = False , user = None , stop_on_errors = True ,
77106 use_pty = False , host_args = None , shell = None ,
78107 encoding = 'utf-8' ):
79108 """Run command on all hosts in parallel, honoring self.pool_size,
80109 and return output dictionary.
81110
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.
84113
85114 More explicitly, function will return after connection and
86115 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,
139168 :raises: :py:class:`TypeError` on not enough host arguments for cmd
140169 string format
141170 :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+ """
143175 return BaseParallelSSHClient .run_command (
144176 self , command , stop_on_errors = stop_on_errors , host_args = host_args ,
145177 user = user , shell = shell , sudo = sudo ,
@@ -184,11 +216,34 @@ def _get_exit_code(self, channel):
184216 return
185217 return channel .get_exit_status ()
186218
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+
187238 def _make_ssh_client (self , host ):
188239 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 )
189242 _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
190245 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 ,
192247 num_retries = self .num_retries , timeout = self .timeout ,
193248 allow_agent = self .allow_agent , retry_delay = self .retry_delay )
194249
0 commit comments