From 82cb76ce94c1272592e479599c5ac78c3d71db7d Mon Sep 17 00:00:00 2001 From: Nicolas Savatier Date: Thu, 5 Jun 2025 17:34:53 +0200 Subject: [PATCH] Add new "scp_socket_timeout" parameter to "Open Connection" This allows to expose the "socket_timeout" parameter of SCPClient. It allows users to define a specific timeout when the connection used is not stable, and default timeout of 10s is too short. --- atest/get_connection.robot | 6 ++++ atest/importing_with_args.robot | 4 ++- src/SSHLibrary/client.py | 28 ++++++++------- src/SSHLibrary/library.py | 64 +++++++++++++++++++++++---------- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/atest/get_connection.robot b/atest/get_connection.robot index 860cecb15..c8da15570 100644 --- a/atest/get_connection.robot +++ b/atest/get_connection.robot @@ -55,6 +55,12 @@ Get Connection Host And Timeout Only Should Be Equal ${rhost} ${HOST} Should Be Equal As Integers ${timeout} 3 +Get Connection Host And Scp Timeout Only + Open Connection ${HOST} scp_socket_timeout=10 seconds + ${rhost} ${scp_socket_timeout} = Get Connection host=Yes scp_socket_timeout=True port=false + Should Be Equal ${rhost} ${HOST} + Should Be Equal As Integers ${scp_socket_timeout} 10 + Get Connections Open Connection ${HOST} prompt=>> escape_ansi=True Open Connection ${HOST} alias=another diff --git a/atest/importing_with_args.robot b/atest/importing_with_args.robot index 95b15754c..9e897eae8 100644 --- a/atest/importing_with_args.robot +++ b/atest/importing_with_args.robot @@ -8,9 +8,11 @@ Importing Library With Arguments [Setup] Open Connections ${conn}= Get Connections Should Be Equal As Integers ${conn[0].timeout} 210 + Should Be Equal As Integers ${conn[0].scp_socket_timeout} 10 Should Be Equal ${conn[0].prompt} >> Should Be Equal ${conn[1].path_separator} \\ Should Be Equal As Integers ${conn[1].timeout} 60 + Should Be Equal As Integers ${conn[1].scp_socket_timeout} 90 Should Be Equal ${conn[1].prompt} >> Should Be Equal ${conn[1].path_separator} \\ [Teardown] Close All Connections @@ -19,5 +21,5 @@ Importing Library With Arguments *** Keywords *** Open Connections Open Connection localhost - Set Default Configuration timeout=1 minute + Set Default Configuration timeout=1 minute scp_socket_timeout=90 seconds Open Connection localhost diff --git a/src/SSHLibrary/client.py b/src/SSHLibrary/client.py index b99f53dcf..cca667f2e 100644 --- a/src/SSHLibrary/client.py +++ b/src/SSHLibrary/client.py @@ -78,7 +78,8 @@ class SSHClientException(RuntimeError): class _ClientConfiguration(Configuration): def __init__(self, host, alias, port, timeout, newline, prompt, term_type, - width, height, path_separator, encoding, escape_ansi, encoding_errors): + width, height, path_separator, encoding, escape_ansi, encoding_errors, + scp_socket_timeout): super(_ClientConfiguration, self).__init__( index=IntegerEntry(None), host=StringEntry(host), @@ -93,7 +94,8 @@ def __init__(self, host, alias, port, timeout, newline, prompt, term_type, path_separator=StringEntry(path_separator), encoding=StringEntry(encoding), escape_ansi=StringEntry(escape_ansi), - encoding_errors=StringEntry(encoding_errors) + encoding_errors=StringEntry(encoding_errors), + scp_socket_timeout=TimeEntry(scp_socket_timeout) ) @@ -107,10 +109,12 @@ class SSHClient(object): def __init__(self, host, alias=None, port=22, timeout=3, newline='LF', prompt=None, term_type='vt100', width=80, height=24, - path_separator='/', encoding='utf8', escape_ansi=False, encoding_errors='strict'): + path_separator='/', encoding='utf8', escape_ansi=False, encoding_errors='strict', + scp_socket_timeout=10): self.config = _ClientConfiguration(host, alias, port, timeout, newline, prompt, term_type, width, height, - path_separator, encoding, escape_ansi, encoding_errors) + path_separator, encoding, escape_ansi, encoding_errors, + scp_socket_timeout) self._sftp_client = None self._scp_transfer_client = None self._scp_all_client = None @@ -250,7 +254,7 @@ def _read_login_output(self, delay): def login_with_public_key(self, username, keyfile, password, allow_agent=False, look_for_keys=False, delay=None, proxy_cmd=None, - jumphost_connection=None, read_config=False, keep_alive_interval='0 seconds', + jumphost_connection=None, read_config=False, keep_alive_interval='0 seconds', disabled_algorithms=None): """Logs into the remote host using the public key authentication. @@ -297,7 +301,7 @@ def login_with_public_key(self, username, keyfile, password, allow_agent=False, self._login_with_public_key(username, keyfile, password, allow_agent, look_for_keys, proxy_cmd, jumphost_connection, - read_config, keep_alive_interval, + read_config, keep_alive_interval, disabled_algorithms) except SSHClientException: self.client.close() @@ -1009,10 +1013,10 @@ def _create_sftp_client(self): return SFTPClient(self.client, self.config.encoding) def _create_scp_transfer_client(self): - return SCPTransferClient(self.client, self.config.encoding) + return SCPTransferClient(self.client, self.config.encoding, self.config.scp_socket_timeout) def _create_scp_all_client(self): - return SCPClient(self.client) + return SCPClient(self.client, self.config.scp_socket_timeout) def _create_shell(self): return Shell(self.client, self.config.term_type, @@ -1584,8 +1588,8 @@ def _readlink(self, path): class SCPClient(object): - def __init__(self, ssh_client): - self._scp_client = scp.SCPClient(ssh_client.get_transport()) + def __init__(self, ssh_client, scp_socket_timeout): + self._scp_client = scp.SCPClient(ssh_client.get_transport(), socket_timeout=scp_socket_timeout) def put_file(self, source, destination, scp_preserve_times, *args): sources = self._get_put_file_sources(source) @@ -1614,8 +1618,8 @@ def _get_put_file_sources(self, source): class SCPTransferClient(SFTPClient): - def __init__(self, ssh_client, encoding): - self._scp_client = scp.SCPClient(ssh_client.get_transport()) + def __init__(self, ssh_client, encoding, scp_socket_timeout): + self._scp_client = scp.SCPClient(ssh_client.get_transport(), socket_timeout=scp_socket_timeout) super(SCPTransferClient, self).__init__(ssh_client, encoding) def _put_file(self, source, destination, mode, newline, path_separator, scp_preserve_times=False): diff --git a/src/SSHLibrary/library.py b/src/SSHLibrary/library.py index c35f684bc..d61fdce21 100644 --- a/src/SSHLibrary/library.py +++ b/src/SSHLibrary/library.py @@ -31,6 +31,7 @@ NewlineEntry, StringEntry, TimeEntry, + ) from .version import VERSION @@ -163,6 +164,16 @@ class SSHLibrary: Argument ``timeout`` is used by `Read Until` variants. The default value is ``3 seconds``. See `time format` below for supported timeout syntax. + === SCP Socket timeout === + Argument ``scp_socket_timeout`` defines the timeout of the socket used for scp operations. + It exposes the 'socket_timeout' parameter of SCPClient. + (See SCPClient __init__() in https://github.com/jbardin/scp.py/blob/master/scp.py) + This allows to specify the timeout that the scp client should use in keywords + `Get File`, `Get Directory`, `Put File`, `Put Directory`, when the parameter ``scp`` + is set to ``TRANSFER`` or ``ALL`. + The default is ``10 seconds``. + See `time format` below for supported scp_socket_timeout syntax. + === Newline === Argument ``newline`` is the line break sequence used by `Write` keyword @@ -449,7 +460,7 @@ class SSHLibrary: DEFAULT_ENCODING = "UTF-8" DEFAULT_ESCAPE_ANSI = False DEFAULT_ENCODING_ERRORS = "strict" - + DEFAULT_SCP_SOCKET_TIMEOUT = "10 seconds" def __init__( self, timeout=DEFAULT_TIMEOUT, @@ -463,6 +474,7 @@ def __init__( encoding=DEFAULT_ENCODING, escape_ansi=DEFAULT_ESCAPE_ANSI, encoding_errors=DEFAULT_ENCODING_ERRORS, + scp_socket_timeout=DEFAULT_SCP_SOCKET_TIMEOUT ): """SSHLibrary allows some import time `configuration`. @@ -502,6 +514,7 @@ def __init__( encoding or self.DEFAULT_ENCODING, escape_ansi or self.DEFAULT_ESCAPE_ANSI, encoding_errors or self.DEFAULT_ENCODING_ERRORS, + scp_socket_timeout or self.DEFAULT_SCP_SOCKET_TIMEOUT ) self._last_commands = dict() @@ -523,6 +536,7 @@ def set_default_configuration( encoding=None, escape_ansi=None, encoding_errors=None, + scp_socket_timeout=None ): """Update the default `configuration`. @@ -566,6 +580,7 @@ def set_default_configuration( encoding=encoding, escape_ansi=escape_ansi, encoding_errors=encoding_errors, + scp_socket_timeout=scp_socket_timeout ) @keyword(tags=("configuration",)) @@ -581,6 +596,7 @@ def set_client_configuration( encoding=None, escape_ansi=None, encoding_errors=None, + scp_socket_timeout=None ): """Update the `configuration` of the current connection. @@ -621,6 +637,7 @@ def set_client_configuration( encoding=encoding, escape_ansi=escape_ansi, encoding_errors=encoding_errors, + scp_socket_timeout=scp_socket_timeout ) @keyword(tags=("configuration",)) @@ -661,6 +678,7 @@ def open_connection( encoding=None, escape_ansi=None, encoding_errors=None, + scp_socket_timeout=None ): """Opens a new SSH connection to the given ``host`` and ``port``. @@ -729,6 +747,7 @@ def open_connection( encoding = encoding or self._config.encoding escape_ansi = escape_ansi or self._config.escape_ansi encoding_errors = encoding_errors or self._config.encoding_errors + scp_socket_timeout = scp_socket_timeout or self._config.scp_socket_timeout client = SSHClient( host, alias, @@ -743,6 +762,7 @@ def open_connection( encoding, escape_ansi, encoding_errors, + scp_socket_timeout ) connection_index = self._connections.register(client, alias) client.config.update(index=connection_index) @@ -833,6 +853,7 @@ def get_connection( height=False, encoding=False, escape_ansi=False, + scp_socket_timeout=False ): """Returns information about the connection. @@ -845,19 +866,20 @@ def get_connection( This keyword returns an object that has the following attributes: - | = Name = | = Type = | = Explanation = | - | index | integer | Number of the connection. Numbering starts from ``1``. | - | host | string | Destination hostname. | - | alias | string | An optional alias given when creating the connection. | - | port | integer | Destination port. | - | timeout | string | `Timeout` length in textual representation. | - | newline | string | The line break sequence used by `Write` keyword. See `newline`. | - | prompt | string | `Prompt` character sequence for `Read Until Prompt`. | - | term_type | string | Type of the virtual terminal. See `terminal settings`. | - | width | integer | Width of the virtual terminal. See `terminal settings`. | - | height | integer | Height of the virtual terminal. See `terminal settings`. | - | path_separator | string | The `path separator` used on the remote host. | - | encoding | string | The `encoding` used for inputs and outputs. | + | = Name = | = Type = | = Explanation = | + | index | integer | Number of the connection. Numbering starts from ``1``. | + | host | string | Destination hostname. | + | alias | string | An optional alias given when creating the connection. | + | port | integer | Destination port. | + | timeout | string | `Timeout` length in textual representation. | + | newline | string | The line break sequence used by `Write` keyword. See `newline`. | + | prompt | string | `Prompt` character sequence for `Read Until Prompt`. | + | term_type | string | Type of the virtual terminal. See `terminal settings`. | + | width | integer | Width of the virtual terminal. See `terminal settings`. | + | height | integer | Height of the virtual terminal. See `terminal settings`. | + | path_separator | string | The `path separator` used on the remote host. | + | encoding | string | The `encoding` used for inputs and outputs. | + | scp_socket_timeout | string | `SCP Socket timeout` length in textual representation. | If there is no connection, an object having ``index`` and ``host`` as ``None`` is returned, rest of its attributes having their values @@ -937,6 +959,7 @@ def get_connection( height, encoding, escape_ansi, + scp_socket_timeout ) ) if not return_values: @@ -985,6 +1008,7 @@ def _get_config_values( height, encoding, escape_ansi, + scp_socket_timeout ): if is_truthy(index): yield config.index @@ -1010,6 +1034,8 @@ def _get_config_values( yield config.encoding if is_truthy(escape_ansi): yield config.escape_ansi + if is_truthy(scp_socket_timeout): + yield config.scp_socket_timeout @keyword(tags=("connection",)) def get_connections(self): @@ -1085,8 +1111,8 @@ def login( ``keep_alive_interval`` is new in SSHLibrary 3.7.0. ``disabled_algorithms`` is a list of algorithms that should be disabled. - For example, if you need to disable diffie-hellman-group16-sha512 key exchange - (perhaps because your code talks to a server which implements it differently from Paramiko), + For example, if you need to disable diffie-hellman-group16-sha512 key exchange + (perhaps because your code talks to a server which implements it differently from Paramiko), specify disabled_algorithms={"kex": ["diffie-hellman-group16-sha512"]} Example that logs in and returns the output: @@ -1213,8 +1239,8 @@ def login_with_public_key( ``keep_alive_interval`` is new in SSHLibrary 3.7.0. ``disabled_algorithms`` is a list of algorithms that should be disabled. - For example, if you need to disable diffie-hellman-group16-sha512 key exchange - (perhaps because your code talks to a server which implements it differently from Paramiko), + For example, if you need to disable diffie-hellman-group16-sha512 key exchange + (perhaps because your code talks to a server which implements it differently from Paramiko), specify disabled_algorithms={"kex": ["diffie-hellman-group16-sha512"]} Example login with disabled algorithms: @@ -2273,6 +2299,7 @@ def __init__( encoding, escape_ansi, encoding_errors, + scp_socket_timeout ): super(_DefaultConfiguration, self).__init__( timeout=TimeEntry(timeout), @@ -2286,4 +2313,5 @@ def __init__( encoding=StringEntry(encoding), escape_ansi=StringEntry(escape_ansi), encoding_errors=StringEntry(encoding_errors), + scp_socket_timeout = TimeEntry(scp_socket_timeout) )