Skip to content

Commit 571272e

Browse files
session: Add 'open_session_retries' option
Improve pylibssh handling when libssh ssh_channel_open_session() returns SSH_AGAIN. Add a new 'open_session_retries' session connect() parameter to allow a configurable number of retries. SSH_AGAIN may be returned when setting a low SSH options timeout value. The default option value is 0, no retries will be attempted.
1 parent 2959959 commit 571272e

File tree

5 files changed

+56
-12
lines changed

5 files changed

+56
-12
lines changed

src/pylibsshext/channel.pyx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ from libc.string cimport memset
2424

2525
from pylibsshext.errors cimport LibsshChannelException
2626
from pylibsshext.errors import LibsshChannelReadFailure
27-
from pylibsshext.session cimport get_libssh_session
27+
from pylibsshext.session cimport get_libssh_session, get_session_retries
2828

2929
from subprocess import CompletedProcess
3030

@@ -63,12 +63,20 @@ cdef class Channel:
6363

6464
if self._libssh_channel is NULL:
6565
raise MemoryError
66-
rc = libssh.ssh_channel_open_session(self._libssh_channel)
6766

68-
if rc != libssh.SSH_OK:
69-
libssh.ssh_channel_free(self._libssh_channel)
70-
self._libssh_channel = NULL
71-
raise LibsshChannelException("Failed to open_session: [%d]" % rc)
67+
retry = get_session_retries(session)
68+
69+
for attempt in range(retry + 1):
70+
rc = libssh.ssh_channel_open_session(self._libssh_channel)
71+
if rc == libssh.SSH_OK:
72+
break
73+
if rc == libssh.SSH_AGAIN and attempt < retry:
74+
continue
75+
# either SSH_ERROR, or SSH_AGAIN with final attempt
76+
if rc != libssh.SSH_OK:
77+
libssh.ssh_channel_free(self._libssh_channel)
78+
self._libssh_channel = NULL
79+
raise LibsshChannelException("Failed to open_session: [%d]" % rc)
7280

7381
def __dealloc__(self):
7482
if self._libssh_channel is not NULL:
@@ -164,10 +172,18 @@ cdef class Channel:
164172
if channel is NULL:
165173
raise MemoryError
166174

167-
rc = libssh.ssh_channel_open_session(channel)
168-
if rc != libssh.SSH_OK:
169-
libssh.ssh_channel_free(channel)
170-
raise LibsshChannelException("Failed to open_session: [{0}]".format(rc))
175+
retry = get_session_retries(self._session)
176+
177+
for attempt in range(retry + 1):
178+
rc = libssh.ssh_channel_open_session(channel)
179+
if rc == libssh.SSH_OK:
180+
break
181+
if rc == libssh.SSH_AGAIN and attempt < retry:
182+
continue
183+
# either SSH_ERROR, or SSH_AGAIN with final attempt
184+
if rc != libssh.SSH_OK:
185+
libssh.ssh_channel_free(channel)
186+
raise LibsshChannelException("Failed to open_session: [{0}]".format(rc))
171187

172188
result = CompletedProcess(args=command, returncode=-1, stdout=b'', stderr=b'')
173189

src/pylibsshext/session.pxd

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ cdef class Session:
2626
cdef _hash_py
2727
cdef _fingerprint_py
2828
cdef _keytype_py
29+
cdef _retries
2930
cdef _channel_callbacks
3031

3132
cdef libssh.ssh_session get_libssh_session(Session session)
33+
cdef int get_session_retries(Session session)

src/pylibsshext/session.pyx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ cdef class Session(object):
108108
self._hash_py = None
109109
self._fingerprint_py = None
110110
self._keytype_py = None
111+
self._retries = 0
111112
# Due to delayed freeing of channels, some older libssh versions might expect
112113
# the callbacks to be around even after we free the underlying channels so
113114
# we should free them only when we terminate the session.
@@ -235,6 +236,11 @@ cdef class Session(object):
235236
file should be validated. It defaults to True
236237
:type host_key_checking: boolean
237238
239+
:param open_session_retries: The number of retries to attempt when libssh
240+
channel function ssh_channel_open_session() returns SSH_AGAIN. It defaults
241+
to 0, no retries attempted.
242+
:type open_session_retries: integer
243+
238244
:param timeout: The timeout in seconds for the TCP connect
239245
:type timeout: long integer
240246
@@ -261,6 +267,9 @@ cdef class Session(object):
261267
libssh.ssh_disconnect(self._libssh_session)
262268
raise
263269

270+
if kwargs.get('open_session_retries'):
271+
self._retries = kwargs.get('open_session_retries')
272+
264273
# We need to userauth_none before we can query the available auth types
265274
rc = libssh.ssh_userauth_none(self._libssh_session, NULL)
266275
if rc == libssh.SSH_AUTH_SUCCESS:
@@ -553,3 +562,6 @@ cdef class Session(object):
553562

554563
cdef libssh.ssh_session get_libssh_session(Session session):
555564
return session._libssh_session
565+
566+
cdef int get_session_retries(Session session):
567+
return session._retries

tests/_service_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def wait_for_svc_ready_state(
6969

7070
def ensure_ssh_session_connected( # noqa: WPS317
7171
ssh_session, sshd_addr, ssh_clientkey_path, # noqa: WPS318
72+
ssh_session_retries=0
7273
):
7374
"""Attempt connecting to the SSH server until successful.
7475
@@ -88,5 +89,5 @@ def ensure_ssh_session_connected( # noqa: WPS317
8889
user=getpass.getuser(),
8990
private_key=ssh_clientkey_path.read_bytes(),
9091
host_key_checking=False,
91-
look_for_keys=False,
92+
open_session_retries=ssh_session_retries,
9293
)

tests/conftest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ def sshd_hostkey_path(sshd_path):
8383
return path
8484

8585

86+
@pytest.fixture
87+
def ssh_session_retries():
88+
"""Return a ssh session retry value
89+
90+
:return: SSH Open session retries
91+
:rtype: int
92+
93+
# noqa: DAR101
94+
"""
95+
return 5
96+
97+
8698
@pytest.fixture
8799
def ssh_clientkey_path(sshd_path):
88100
"""Generate an SSH keypair.
@@ -127,7 +139,7 @@ def ssh_client_session(ssh_session_connect):
127139

128140

129141
@pytest.fixture
130-
def ssh_session_connect(sshd_addr, ssh_clientkey_path):
142+
def ssh_session_connect(sshd_addr, ssh_clientkey_path, ssh_session_retries):
131143
"""
132144
Authenticate existing session object against SSHD with a private SSH key.
133145
@@ -140,6 +152,7 @@ def ssh_session_connect(sshd_addr, ssh_clientkey_path):
140152
ensure_ssh_session_connected,
141153
sshd_addr=sshd_addr,
142154
ssh_clientkey_path=ssh_clientkey_path,
155+
ssh_session_retries=ssh_session_retries,
143156
)
144157

145158

0 commit comments

Comments
 (0)