@@ -128,6 +128,8 @@ def __call__( # type: ignore
128128 private_keys : typing .Optional [typing .Iterable [paramiko .RSAKey ]] = None ,
129129 auth : typing .Optional [ssh_auth .SSHAuth ] = None ,
130130 verbose : bool = True ,
131+ * ,
132+ chroot_path : typing .Optional [str ] = None ,
131133 ) -> "SSHClientBase" :
132134 """Main memorize method: check for cached instance and return it. API follows target __init__.
133135
@@ -145,10 +147,12 @@ def __call__( # type: ignore
145147 :type auth: typing.Optional[ssh_auth.SSHAuth]
146148 :param verbose: show additional error/warning messages
147149 :type verbose: bool
150+ :param chroot_path: chroot path (use chroot if set)
151+ :type chroot_path: typing.Optional[str]
148152 :return: SSH client instance
149153 :rtype: SSHClientBase
150154 """
151- if (host , port ) in cls .__cache :
155+ if (host , port ) in cls .__cache and not chroot_path : # chrooted connections are not memorized
152156 key = host , port
153157 if auth is None :
154158 auth = ssh_auth .SSHAuth (username = username , password = password , keys = private_keys )
@@ -176,8 +180,10 @@ def __call__( # type: ignore
176180 private_keys = private_keys ,
177181 auth = auth ,
178182 verbose = verbose ,
183+ chroot_path = chroot_path ,
179184 )
180- cls .__cache [(ssh .hostname , ssh .port )] = ssh
185+ if not chroot_path :
186+ cls .__cache [(ssh .hostname , ssh .port )] = ssh
181187 return ssh
182188
183189 @classmethod
@@ -188,7 +194,6 @@ def clear_cache(mcs: typing.Type["_MemorizedSSH"]) -> None:
188194 """
189195 n_count = 3
190196 # PY3: cache, ssh, temporary
191- # PY4: cache, values mapping, ssh, temporary
192197 for ssh in mcs .__cache .values ():
193198 if CPYTHON and sys .getrefcount (ssh ) == n_count : # pragma: no cover
194199 ssh .logger .debug ("Closing as unused" )
@@ -203,63 +208,66 @@ def close_connections(mcs: typing.Type["_MemorizedSSH"]) -> None:
203208 ssh .close () # type: ignore
204209
205210
206- class SSHClientBase ( api . ExecHelper , metaclass = _MemorizedSSH ) :
207- """SSH Client helper ."""
211+ class _SudoContext :
212+ """Context manager for call commands with sudo ."""
208213
209- __slots__ = ("__hostname" , "__port" , "__auth" , " __ssh" , "__sftp " , "__sudo_mode" , "__keepalive_mode" , "__verbose " )
214+ __slots__ = ("__ssh" , "__sudo_status " , "__enforce " )
210215
211- class __get_sudo :
212- """Context manager for call commands with sudo."""
216+ def __init__ ( self , ssh : "SSHClientBase" , enforce : typing . Optional [ bool ] = None ) -> None :
217+ """Context manager for call commands with sudo.
213218
214- __slots__ = ("__ssh" , "__sudo_status" , "__enforce" )
219+ :param ssh: connection instance
220+ :type ssh: SSHClientBase
221+ :param enforce: sudo mode for context manager
222+ :type enforce: typing.Optional[bool]
223+ """
224+ self .__ssh : "SSHClientBase" = ssh
225+ self .__sudo_status : bool = ssh .sudo_mode
226+ self .__enforce : typing .Optional [bool ] = enforce
215227
216- def __init__ (self , ssh : "SSHClientBase" , enforce : typing .Optional [bool ] = None ) -> None :
217- """Context manager for call commands with sudo.
228+ def __enter__ (self ) -> None :
229+ self .__sudo_status = self .__ssh .sudo_mode
230+ if self .__enforce is not None :
231+ self .__ssh .sudo_mode = self .__enforce
218232
219- :param ssh: connection instance
220- :type ssh: SSHClientBase
221- :param enforce: sudo mode for context manager
222- :type enforce: typing.Optional[bool]
223- """
224- self .__ssh : "SSHClientBase" = ssh
225- self .__sudo_status : bool = ssh .sudo_mode
226- self .__enforce : typing .Optional [bool ] = enforce
233+ def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
234+ self .__ssh .sudo_mode = self .__sudo_status
227235
228- def __enter__ (self ) -> None :
229- self .__sudo_status = self .__ssh .sudo_mode
230- if self .__enforce is not None :
231- self .__ssh .sudo_mode = self .__enforce
232236
233- def __exit__ ( self , exc_type : typing . Any , exc_val : typing . Any , exc_tb : typing . Any ) -> None :
234- self . __ssh . sudo_mode = self . __sudo_status
237+ class _KeepAliveContext :
238+ """Context manager for keepalive management."""
235239
236- class __get_keepalive :
237- """Context manager for keepalive management."""
240+ __slots__ = ("__ssh" , "__keepalive_status" , "__enforce" )
238241
239- __slots__ = ("__ssh" , "__keepalive_status" , "__enforce" )
242+ def __init__ (self , ssh : "SSHClientBase" , enforce : bool = True ) -> None :
243+ """Context manager for keepalive management.
240244
241- def __init__ (self , ssh : "SSHClientBase" , enforce : bool = True ) -> None :
242- """Context manager for keepalive management.
245+ :param ssh: connection instance
246+ :type ssh: SSHClientBase
247+ :param enforce: keepalive mode for context manager
248+ :type enforce: bool
249+ :param enforce: Keep connection alive after context manager exit
250+ """
251+ self .__ssh : "SSHClientBase" = ssh
252+ self .__keepalive_status : bool = ssh .keepalive_mode
253+ self .__enforce : typing .Optional [bool ] = enforce
243254
244- :param ssh: connection instance
245- :type ssh: SSHClientBase
246- :param enforce: keepalive mode for context manager
247- :type enforce: bool
248- :param enforce: Keep connection alive after context manager exit
249- """
250- self .__ssh : "SSHClientBase" = ssh
251- self .__keepalive_status : bool = ssh .keepalive_mode
252- self .__enforce : typing .Optional [bool ] = enforce
255+ def __enter__ (self ) -> None :
256+ self .__ssh .__enter__ ()
257+ self .__keepalive_status = self .__ssh .keepalive_mode
258+ if self .__enforce is not None :
259+ self .__ssh .keepalive_mode = self .__enforce
253260
254- def __enter__ (self ) -> None :
255- self .__keepalive_status = self .__ssh .keepalive_mode
256- if self .__enforce is not None :
257- self .__ssh .keepalive_mode = self .__enforce
258- self .__ssh .__enter__ ()
261+ def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
262+ # Exit before releasing!
263+ self .__ssh .__exit__ (exc_type = exc_type , exc_val = exc_val , exc_tb = exc_tb ) # type: ignore
264+ self .__ssh .keepalive_mode = self .__keepalive_status
265+
266+
267+ class SSHClientBase (api .ExecHelper , metaclass = _MemorizedSSH ):
268+ """SSH Client helper."""
259269
260- def __exit__ (self , exc_type : typing .Any , exc_val : typing .Any , exc_tb : typing .Any ) -> None :
261- self .__ssh .__exit__ (exc_type = exc_type , exc_val = exc_val , exc_tb = exc_tb ) # type: ignore
262- self .__ssh .keepalive_mode = self .__keepalive_status
270+ __slots__ = ("__hostname" , "__port" , "__auth" , "__ssh" , "__sftp" , "__sudo_mode" , "__keepalive_mode" , "__verbose" )
263271
264272 def __hash__ (self ) -> int :
265273 """Hash for usage as dict keys."""
@@ -274,6 +282,8 @@ def __init__(
274282 private_keys : typing .Optional [typing .Iterable [paramiko .RSAKey ]] = None ,
275283 auth : typing .Optional [ssh_auth .SSHAuth ] = None ,
276284 verbose : bool = True ,
285+ * ,
286+ chroot_path : typing .Optional [str ] = None ,
277287 ) -> None :
278288 """Main SSH Client helper.
279289
@@ -291,11 +301,13 @@ def __init__(
291301 :type auth: typing.Optional[ssh_auth.SSHAuth]
292302 :param verbose: show additional error/warning messages
293303 :type verbose: bool
304+ :param chroot_path: chroot path (use chroot if set)
305+ :type chroot_path: typing.Optional[str]
294306
295307 .. note:: auth has priority over username/password/private_keys
296308 """
297309 super (SSHClientBase , self ).__init__ (
298- logger = logging .getLogger (self .__class__ .__name__ ).getChild (f"{ host } :{ port } " )
310+ logger = logging .getLogger (self .__class__ .__name__ ).getChild (f"{ host } :{ port } " ), chroot_path = chroot_path
299311 )
300312
301313 self .__hostname : str = host
@@ -510,7 +522,7 @@ def sudo(self, enforce: typing.Optional[bool] = None) -> "typing.ContextManager[
510522 :return: context manager with selected sudo state inside
511523 :rtype: typing.ContextManager
512524 """
513- return self . __get_sudo (ssh = self , enforce = enforce )
525+ return _SudoContext (ssh = self , enforce = enforce )
514526
515527 def keepalive (self , enforce : bool = True ) -> "typing.ContextManager[None]" :
516528 """Call contextmanager with keepalive mode change.
@@ -523,7 +535,7 @@ def keepalive(self, enforce: bool = True) -> "typing.ContextManager[None]":
523535 .. Note:: Enter and exit ssh context manager is produced as well.
524536 .. versionadded:: 1.2.1
525537 """
526- return self . __get_keepalive (ssh = self , enforce = enforce )
538+ return _KeepAliveContext (ssh = self , enforce = enforce )
527539
528540 # noinspection PyMethodOverriding
529541 def execute_async ( # pylint: disable=arguments-differ
@@ -535,6 +547,7 @@ def execute_async( # pylint: disable=arguments-differ
535547 verbose : bool = False ,
536548 log_mask_re : typing .Optional [str ] = None ,
537549 * ,
550+ chroot_path : typing .Optional [str ] = None ,
538551 get_pty : bool = False ,
539552 width : int = 80 ,
540553 height : int = 24 ,
@@ -555,6 +568,8 @@ def execute_async( # pylint: disable=arguments-differ
555568 :param log_mask_re: regex lookup rule to mask command for logger.
556569 all MATCHED groups will be replaced by '<*masked*>'
557570 :type log_mask_re: typing.Optional[str]
571+ :param chroot_path: chroot path override
572+ :type chroot_path: typing.Optional[str]
558573 :param get_pty: Get PTY for connection
559574 :type get_pty: bool
560575 :param width: PTY width
@@ -580,6 +595,7 @@ def execute_async( # pylint: disable=arguments-differ
580595 .. versionchanged:: 1.2.0 get_pty moved to `**kwargs`
581596 .. versionchanged:: 2.1.0 Use typed NamedTuple as result
582597 .. versionchanged:: 3.2.0 Expose pty options as optional keyword-only arguments
598+ .. versionchanged:: 4.1.0 support chroot
583599 """
584600 cmd_for_log : str = self ._mask_command (cmd = command , log_mask_re = log_mask_re )
585601
@@ -597,7 +613,8 @@ def execute_async( # pylint: disable=arguments-differ
597613 stdout : paramiko .ChannelFile = chan .makefile ("rb" )
598614 stderr : typing .Optional [paramiko .ChannelFile ] = chan .makefile_stderr ("rb" ) if open_stderr else None
599615
600- cmd = f"{ command } \n "
616+ cmd = f"{ self ._prepare_command (cmd = command , chroot_path = chroot_path )} \n "
617+
601618 started = datetime .datetime .utcnow ()
602619 if self .sudo_mode :
603620 encoded_cmd = base64 .b64encode (cmd .encode ("utf-8" )).decode ("utf-8" )
0 commit comments