@@ -139,6 +139,8 @@ def __call__( # type: ignore
139139 private_keys : typing .Optional [typing .Iterable [paramiko .RSAKey ]] = None ,
140140 auth : typing .Optional [ssh_auth .SSHAuth ] = None ,
141141 verbose : bool = True ,
142+ * ,
143+ chroot_path : typing .Optional [str ] = None
142144 ) -> "SSHClientBase" :
143145 """Main memorize method: check for cached instance and return it. API follows target __init__.
144146
@@ -156,10 +158,12 @@ def __call__( # type: ignore
156158 :type auth: typing.Optional[ssh_auth.SSHAuth]
157159 :param verbose: show additional error/warning messages
158160 :type verbose: bool
161+ :param chroot_path: chroot path (use chroot if set)
162+ :type chroot_path: typing.Optional[str]
159163 :return: SSH client instance
160164 :rtype: SSHClientBase
161165 """
162- if (host , port ) in cls .__cache :
166+ if (host , port ) in cls .__cache and not chroot_path : # chrooted connections are not memorized
163167 key = host , port
164168 if auth is None :
165169 auth = ssh_auth .SSHAuth (username = username , password = password , keys = private_keys )
@@ -187,8 +191,10 @@ def __call__( # type: ignore
187191 private_keys = private_keys ,
188192 auth = auth ,
189193 verbose = verbose ,
194+ chroot_path = chroot_path ,
190195 )
191- cls .__cache [(ssh .hostname , ssh .port )] = ssh
196+ if not chroot_path :
197+ cls .__cache [(ssh .hostname , ssh .port )] = ssh
192198 return ssh
193199
194200 @classmethod
@@ -199,7 +205,6 @@ def clear_cache(mcs: typing.Type["_MemorizedSSH"]) -> None:
199205 """
200206 n_count = 3
201207 # PY3: cache, ssh, temporary
202- # PY4: cache, values mapping, ssh, temporary
203208 for ssh in mcs .__cache .values ():
204209 if CPYTHON and sys .getrefcount (ssh ) == n_count : # pragma: no cover
205210 ssh .logger .debug ("Closing as unused" )
@@ -214,63 +219,66 @@ def close_connections(mcs: typing.Type["_MemorizedSSH"]) -> None:
214219 ssh .close () # type: ignore
215220
216221
217- class SSHClientBase ( api . ExecHelper , metaclass = _MemorizedSSH ) :
218- """SSH Client helper ."""
222+ class _SudoContext :
223+ """Context manager for call commands with sudo ."""
219224
220- __slots__ = ("__hostname" , "__port" , "__auth" , "__ssh" , "__sftp" , "__sudo_mode" , "__keepalive_mode" , "__verbose" )
225+ __slots__ = ("__ssh" , "__sudo_status" , "__enforce" )
226+
227+ def __init__ (self , ssh : "SSHClientBase" , enforce : typing .Optional [bool ] = None ) -> None :
228+ """Context manager for call commands with sudo.
229+
230+ :param ssh: connection instance
231+ :type ssh: SSHClientBase
232+ :param enforce: sudo mode for context manager
233+ :type enforce: typing.Optional[bool]
234+ """
235+ self .__ssh = ssh # type: SSHClientBase
236+ self .__sudo_status = ssh .sudo_mode # type: bool
237+ self .__enforce = enforce # type: typing.Optional[bool]
221238
222- class __get_sudo :
223- """Context manager for call commands with sudo."""
239+ def __enter__ (self ) -> None :
240+ self .__sudo_status = self .__ssh .sudo_mode
241+ if self .__enforce is not None :
242+ self .__ssh .sudo_mode = self .__enforce
224243
225- __slots__ = ("__ssh" , "__sudo_status" , "__enforce" )
244+ def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
245+ self .__ssh .sudo_mode = self .__sudo_status
226246
227- def __init__ (self , ssh : "SSHClientBase" , enforce : typing .Optional [bool ] = None ) -> None :
228- """Context manager for call commands with sudo.
229247
230- :param ssh: connection instance
231- :type ssh: SSHClientBase
232- :param enforce: sudo mode for context manager
233- :type enforce: typing.Optional[bool]
234- """
235- self .__ssh = ssh
236- self .__sudo_status = ssh .sudo_mode
237- self .__enforce = enforce
248+ class _KeepAliveContext :
249+ """Context manager for keepalive management."""
238250
239- def __enter__ (self ) -> None :
240- self .__sudo_status = self .__ssh .sudo_mode
241- if self .__enforce is not None :
242- self .__ssh .sudo_mode = self .__enforce
251+ __slots__ = ("__ssh" , "__keepalive_status" , "__enforce" )
243252
244- def __exit__ (self , exc_type : typing . Any , exc_val : typing . Any , exc_tb : typing . Any ) -> None :
245- self . __ssh . sudo_mode = self . __sudo_status
253+ def __init__ (self , ssh : "SSHClientBase" , enforce : bool = True ) -> None :
254+ """Context manager for keepalive management.
246255
247- class __get_keepalive :
248- """Context manager for keepalive management."""
256+ :param ssh: connection instance
257+ :type ssh: SSHClientBase
258+ :param enforce: keepalive mode for context manager
259+ :type enforce: bool
260+ :param enforce: Keep connection alive after context manager exit
261+ """
262+ self .__ssh = ssh # type: SSHClientBase
263+ self .__keepalive_status = ssh .keepalive_mode # type: bool
264+ self .__enforce = enforce # type: typing.Optional[bool]
249265
250- __slots__ = ("__ssh" , "__keepalive_status" , "__enforce" )
266+ def __enter__ (self ) -> None :
267+ self .__ssh .__enter__ ()
268+ self .__keepalive_status = self .__ssh .keepalive_mode
269+ if self .__enforce is not None :
270+ self .__ssh .keepalive_mode = self .__enforce
251271
252- def __init__ (self , ssh : "SSHClientBase" , enforce : bool = True ) -> None :
253- """Context manager for keepalive management.
272+ def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
273+ # Exit before releasing!
274+ self .__ssh .__exit__ (exc_type = exc_type , exc_val = exc_val , exc_tb = exc_tb ) # type: ignore
275+ self .__ssh .keepalive_mode = self .__keepalive_status
254276
255- :param ssh: connection instance
256- :type ssh: SSHClientBase
257- :param enforce: keepalive mode for context manager
258- :type enforce: bool
259- :param enforce: Keep connection alive after context manager exit
260- """
261- self .__ssh = ssh
262- self .__keepalive_status = ssh .keepalive_mode
263- self .__enforce = enforce
264277
265- def __enter__ (self ) -> None :
266- self .__keepalive_status = self .__ssh .keepalive_mode
267- if self .__enforce is not None :
268- self .__ssh .keepalive_mode = self .__enforce
269- self .__ssh .__enter__ ()
278+ class SSHClientBase (api .ExecHelper , metaclass = _MemorizedSSH ):
279+ """SSH Client helper."""
270280
271- def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
272- self .__ssh .__exit__ (exc_type = exc_type , exc_val = exc_val , exc_tb = exc_tb ) # type: ignore
273- self .__ssh .keepalive_mode = self .__keepalive_status
281+ __slots__ = ("__hostname" , "__port" , "__auth" , "__ssh" , "__sftp" , "__sudo_mode" , "__keepalive_mode" , "__verbose" )
274282
275283 def __hash__ (self ) -> int :
276284 """Hash for usage as dict keys."""
@@ -285,6 +293,8 @@ def __init__(
285293 private_keys : typing .Optional [typing .Iterable [paramiko .RSAKey ]] = None ,
286294 auth : typing .Optional [ssh_auth .SSHAuth ] = None ,
287295 verbose : bool = True ,
296+ * ,
297+ chroot_path : typing .Optional [str ] = None
288298 ) -> None :
289299 """Main SSH Client helper.
290300
@@ -302,11 +312,14 @@ def __init__(
302312 :type auth: typing.Optional[ssh_auth.SSHAuth]
303313 :param verbose: show additional error/warning messages
304314 :type verbose: bool
315+ :param chroot_path: chroot path (use chroot if set)
316+ :type chroot_path: typing.Optional[str]
305317
306318 .. note:: auth has priority over username/password/private_keys
307319 """
308320 super (SSHClientBase , self ).__init__ (
309- logger = logging .getLogger (self .__class__ .__name__ ).getChild ("{host}:{port}" .format (host = host , port = port ))
321+ logger = logging .getLogger (self .__class__ .__name__ ).getChild ("{host}:{port}" .format (host = host , port = port )),
322+ chroot_path = chroot_path
310323 )
311324
312325 self .__hostname = host
@@ -525,7 +538,7 @@ def sudo(self, enforce: typing.Optional[bool] = None) -> "typing.ContextManager[
525538 :return: context manager with selected sudo state inside
526539 :rtype: typing.ContextManager
527540 """
528- return self . __get_sudo (ssh = self , enforce = enforce )
541+ return _SudoContext (ssh = self , enforce = enforce )
529542
530543 def keepalive (self , enforce : bool = True ) -> "typing.ContextManager[None]" :
531544 """Call contextmanager with keepalive mode change.
@@ -538,7 +551,7 @@ def keepalive(self, enforce: bool = True) -> "typing.ContextManager[None]":
538551 .. Note:: Enter and exit ssh context manager is produced as well.
539552 .. versionadded:: 1.2.1
540553 """
541- return self . __get_keepalive (ssh = self , enforce = enforce )
554+ return _KeepAliveContext (ssh = self , enforce = enforce )
542555
543556 # noinspection PyMethodOverriding
544557 def execute_async ( # pylint: disable=arguments-differ
@@ -550,6 +563,7 @@ def execute_async( # pylint: disable=arguments-differ
550563 verbose : bool = False ,
551564 log_mask_re : typing .Optional [str ] = None ,
552565 * ,
566+ chroot_path : typing .Optional [str ] = None ,
553567 get_pty : bool = False ,
554568 width : int = 80 ,
555569 height : int = 24 ,
@@ -570,6 +584,8 @@ def execute_async( # pylint: disable=arguments-differ
570584 :param log_mask_re: regex lookup rule to mask command for logger.
571585 all MATCHED groups will be replaced by '<*masked*>'
572586 :type log_mask_re: typing.Optional[str]
587+ :param chroot_path: chroot path override
588+ :type chroot_path: typing.Optional[str]
573589 :param get_pty: Get PTY for connection
574590 :type get_pty: bool
575591 :param width: PTY width
@@ -595,6 +611,7 @@ def execute_async( # pylint: disable=arguments-differ
595611 .. versionchanged:: 1.2.0 get_pty moved to `**kwargs`
596612 .. versionchanged:: 2.1.0 Use typed NamedTuple as result
597613 .. versionchanged:: 2.9.3 Expose pty options as optional keyword-only arguments
614+ .. versionchanged:: 2.12.0 support chroot
598615 """
599616 cmd_for_log = self ._mask_command (cmd = command , log_mask_re = log_mask_re )
600617
@@ -612,7 +629,7 @@ def execute_async( # pylint: disable=arguments-differ
612629 stdout = chan .makefile ("rb" ) # type: paramiko.ChannelFile
613630 stderr = chan .makefile_stderr ("rb" ) if open_stderr else None
614631
615- cmd = "{command }\n " .format (command = command )
632+ cmd = "{cmd }\n " .format (cmd = self . _prepare_command ( cmd = command , chroot_path = chroot_path ) )
616633 started = datetime .datetime .utcnow ()
617634 if self .sudo_mode :
618635 encoded_cmd = base64 .b64encode (cmd .encode ("utf-8" )).decode ("utf-8" )
0 commit comments