diff --git a/openwisp_controller/connection/connectors/ssh.py b/openwisp_controller/connection/connectors/ssh.py index 67240837f..61850054e 100644 --- a/openwisp_controller/connection/connectors/ssh.py +++ b/openwisp_controller/connection/connectors/ssh.py @@ -1,5 +1,6 @@ import logging import socket +import time from io import BytesIO, StringIO import paramiko @@ -186,6 +187,7 @@ def exec_command( logger.info("Executing command: {0}".format(command)) # execute commmand try: + start_cmd = time.perf_counter() stdin, stdout, stderr = self.shell.exec_command(command, timeout=timeout) # re-raise socket.timeout to avoid being catched # by the subsequent `except Exception as e` block @@ -196,7 +198,14 @@ def exec_command( logger.exception(e) raise e # store command exit status - exit_status = stdout.channel.recv_exit_status() + # workaround https://github.com/paramiko/paramiko/issues/1815 + # workaround https://github.com/paramiko/paramiko/issues/1787 + # Ref. https://docs.paramiko.org/en/stable/api/channel.html#paramiko.channel.Channel.recv_exit_status # noqa + stdout.channel.status_event.wait( + timeout=timeout - int(time.perf_counter() - start_cmd) + ) + assert stdout.channel.status_event.is_set() + exit_status = stdout.channel.exit_status # log standard output # try to decode to UTF-8, ignoring unconvertible characters # https://docs.python.org/3/howto/unicode.html#the-string-type diff --git a/openwisp_controller/connection/tests/test_models.py b/openwisp_controller/connection/tests/test_models.py index 6db7162e3..14693dfbd 100644 --- a/openwisp_controller/connection/tests/test_models.py +++ b/openwisp_controller/connection/tests/test_models.py @@ -1,5 +1,6 @@ import socket from unittest import mock +from unittest.mock import PropertyMock import paramiko from django.contrib.auth.models import ContentType @@ -46,7 +47,7 @@ def _exec_command_return_value( stderr_ = mock.Mock() stdin_.read().decode.return_value = stdin stdout_.read().decode.return_value = stdout - stdout_.channel.recv_exit_status.return_value = exit_code + type(stdout_.channel).exit_status = PropertyMock(return_value=exit_code) stderr_.read().decode.return_value = stderr return (stdin_, stdout_, stderr_) @@ -1009,7 +1010,7 @@ def _assert_applying_conf_test_command(mocked_exec): # 1. Checking openwisp_config returns with 0 # 2. Testing presence of /tmp/openwisp/applying_conf returns with 1 # 3. Restarting openwisp_config returns with 0 exit code - stdout.channel.recv_exit_status.side_effect = [0, 1, 1] + type(stdout.channel).exit_status = PropertyMock(side_effect=[0, 1, 1]) mocked_exec_command.return_value = (stdin, stdout, stderr) conf.save() self.assertEqual(mocked_exec_command.call_count, 3)