Skip to content

Commit 1682a67

Browse files
committed
Merge remote-tracking branch 'origin/master' into issues/1049-send-generic-message
2 parents 3e33d03 + 6dba01d commit 1682a67

File tree

16 files changed

+90
-66
lines changed

16 files changed

+90
-66
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ jobs:
7979
pip install -U -e .
8080
pip install ${{ matrix.django-version }}
8181
82-
- name: Start postgres and redis
82+
- name: Start redis
8383
if: ${{ !cancelled() && steps.deps.conclusion == 'success' }}
84-
run: docker compose up -d postgres redis
84+
run: docker compose up -d redis
8585

8686
- name: QA checks
8787
run: ./run-qa-checks

docker-compose.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,3 @@ services:
1717
ports:
1818
- "6379:6379"
1919
entrypoint: redis-server --appendonly yes
20-
21-
postgres:
22-
image: postgis/postgis:17-3.5-alpine
23-
environment:
24-
POSTGRES_PASSWORD: openwisp2
25-
POSTGRES_USER: openwisp2
26-
POSTGRES_DB: openwisp2
27-
ports:
28-
- 5432:5432

docs/developer/installation.rst

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ Navigate into the cloned repository:
3737
3838
cd openwisp-controller/
3939
40-
Launch Redis and PostgreSQL:
40+
Launch Redis:
4141

4242
.. code-block:: shell
4343
44-
docker compose up -d redis postgres
44+
docker compose up -d redis
4545
4646
Setup and activate a virtual-environment (we'll be using `virtualenv
4747
<https://pypi.org/project/virtualenv/>`_):
@@ -99,18 +99,6 @@ Run tests with (make sure you have the :ref:`selenium dependencies
9999
100100
./runtests
101101
102-
Some tests, such as the Selenium UI tests, require a PostgreSQL database
103-
to run. If you don't have a PostgreSQL database running on your system,
104-
you can use :ref:`the Docker Compose configuration provided in this
105-
repository <controller_dev_docker>`. Once set up, you can run these
106-
specific tests as follows:
107-
108-
.. code-block:: shell
109-
110-
# Run only specific selenium tests classes
111-
cd tests/
112-
DJANGO_SETTINGS_MODULE=openwisp2.postgresql_settings ./manage.py test openwisp_controller.config.tests.test_selenium.TestDeviceAdmin
113-
114102
Run quality assurance tests with:
115103

116104
.. code-block:: shell

openwisp_controller/config/tasks.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ def invalidate_devicegroup_cache_delete(instance_id, model_name, **kwargs):
119119
)
120120

121121

122+
123+
# Generating large configurations can be time-consuming; hence,
124+
# a custom soft time limit is applied here.
125+
122126
@shared_task(soft_time_limit=300)
123127
def trigger_vpn_server_endpoint(endpoint, auth_token, vpn_id):
124128
Vpn = load_model('config', 'Vpn')

openwisp_controller/connection/connectors/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ class CommandFailedException(Exception):
44
"""
55

66
pass
7+
8+
9+
class CommandTimeoutException(Exception):
10+
"""
11+
raised when a command times out
12+
"""
13+
14+
pass

openwisp_controller/connection/connectors/ssh.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import socket
3+
import time
34
from io import BytesIO, StringIO
45

56
import paramiko
@@ -10,7 +11,7 @@
1011
from scp import SCPClient
1112

1213
from .. import settings as app_settings
13-
from .exceptions import CommandFailedException
14+
from .exceptions import CommandFailedException, CommandTimeoutException
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -148,7 +149,7 @@ def _connect(self, address):
148149
auth_timeout=app_settings.SSH_AUTH_TIMEOUT,
149150
banner_timeout=app_settings.SSH_BANNER_TIMEOUT,
150151
timeout=app_settings.SSH_CONNECTION_TIMEOUT,
151-
**params
152+
**params,
152153
)
153154
except paramiko.ssh_exception.AuthenticationException as e:
154155
# the authentication failure may be caused by the issue
@@ -183,9 +184,12 @@ def exec_command(
183184
- aborts on exceptions
184185
- raises socket.timeout exceptions
185186
"""
187+
# paramiko expects timeout as a float
188+
timeout = float(timeout)
186189
logger.info("Executing command: {0}".format(command))
187190
# execute commmand
188191
try:
192+
start_cmd = time.perf_counter()
189193
stdin, stdout, stderr = self.shell.exec_command(command, timeout=timeout)
190194
# re-raise socket.timeout to avoid being catched
191195
# by the subsequent `except Exception as e` block
@@ -195,8 +199,16 @@ def exec_command(
195199
except Exception as e:
196200
logger.exception(e)
197201
raise e
198-
# store command exit status
199-
exit_status = stdout.channel.recv_exit_status()
202+
# workaround https://github.com/paramiko/paramiko/issues/1815
203+
# workaround https://github.com/paramiko/paramiko/issues/1787
204+
# Ref. https://docs.paramiko.org/en/stable/api/channel.html#paramiko.channel.Channel.recv_exit_status # noqa
205+
if not stdout.channel.status_event.wait(
206+
timeout=timeout - (time.perf_counter() - start_cmd)
207+
):
208+
log_message = f"Command timed out after {timeout} seconds."
209+
logger.info(log_message)
210+
raise CommandTimeoutException(log_message)
211+
exit_status = stdout.channel.exit_status
200212
# log standard output
201213
# try to decode to UTF-8, ignoring unconvertible characters
202214
# https://docs.python.org/3/howto/unicode.html#the-string-type

openwisp_controller/connection/tasks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from swapper import load_model
1010

1111
from . import settings as app_settings
12+
from .connectors.exceptions import CommandTimeoutException
1213
from .exceptions import NoWorkingDeviceConnectionError
1314

1415
logger = logging.getLogger(__name__)
@@ -76,6 +77,10 @@ def launch_command(command_id):
7677
command.status = "failed"
7778
command._add_output(_("Background task time limit exceeded."))
7879
command.save()
80+
except CommandTimeoutException as e:
81+
command.status = "failed"
82+
command._add_output(_(f"The command took longer than expected: {e}"))
83+
command.save()
7984
except Exception as e:
8085
logger.exception(
8186
f"An exception was raised while executing command {command_id}"

openwisp_controller/connection/tests/test_models.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import socket
22
from unittest import mock
3+
from unittest.mock import PropertyMock
34

45
import paramiko
56
from django.contrib.auth.models import ContentType
@@ -46,7 +47,7 @@ def _exec_command_return_value(
4647
stderr_ = mock.Mock()
4748
stdin_.read().decode.return_value = stdin
4849
stdout_.read().decode.return_value = stdout
49-
stdout_.channel.recv_exit_status.return_value = exit_code
50+
type(stdout_.channel).exit_status = PropertyMock(return_value=exit_code)
5051
stderr_.read().decode.return_value = stderr
5152
return (stdin_, stdout_, stderr_)
5253

@@ -1009,7 +1010,7 @@ def _assert_applying_conf_test_command(mocked_exec):
10091010
# 1. Checking openwisp_config returns with 0
10101011
# 2. Testing presence of /tmp/openwisp/applying_conf returns with 1
10111012
# 3. Restarting openwisp_config returns with 0 exit code
1012-
stdout.channel.recv_exit_status.side_effect = [0, 1, 1]
1013+
type(stdout.channel).exit_status = PropertyMock(side_effect=[0, 1, 1])
10131014
mocked_exec_command.return_value = (stdin, stdout, stderr)
10141015
conf.save()
10151016
self.assertEqual(mocked_exec_command.call_count, 3)

openwisp_controller/connection/tests/test_ssh.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@ def test_connection_failed_command(self, mocked_debug, mocked_info):
7070
]
7171
)
7272

73+
@mock.patch.object(ssh_logger, "info")
74+
@mock.patch.object(ssh_logger, "debug")
75+
def test_connection_command_timeout(self, mocked_debug, mocked_info):
76+
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
77+
dc = self._create_device_connection(credentials=ckey)
78+
dc.connector_instance.connect()
79+
with self.assertRaises(Exception) as ctx:
80+
# timeout of 0.0 is a special case in paramiko -> we check for 0.01 instead
81+
dc.connector_instance.exec_command("sleep 1", timeout=0.01)
82+
log_message = "Command timed out after 0.01 seconds."
83+
mocked_info.assert_has_calls([mock.call(log_message)])
84+
self.assertEqual(str(ctx.exception), log_message)
85+
7386
@mock.patch.object(ssh_logger, "info")
7487
@mock.patch.object(ssh_logger, "debug")
7588
def test_connection_failed_command_suppressed_output(

openwisp_controller/geo/tests/test_selenium.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
from channels.testing import ChannelsLiveServerTestCase
12
from django.contrib.auth import get_user_model
23
from django.contrib.auth.models import Permission
3-
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
44
from django.test import tag
55
from django.urls.base import reverse
66
from django_loci.tests import TestAdminMixin
@@ -22,7 +22,10 @@
2222
# these tests are for geo elements on device admin
2323
@tag("selenium_tests")
2424
class TestDeviceAdminGeoSelenium(
25-
BaseTestDeviceAdminSelenium, TestOrganizationMixin, StaticLiveServerTestCase
25+
TestGeoMixin,
26+
BaseTestDeviceAdminSelenium,
27+
TestOrganizationMixin,
28+
ChannelsLiveServerTestCase,
2629
):
2730
app_label = "geo"
2831
object_model = Device
@@ -66,7 +69,7 @@ class TestDeviceAdminReadonly(
6669
TestGeoMixin,
6770
TestAdminMixin,
6871
SeleniumTestMixin,
69-
StaticLiveServerTestCase,
72+
ChannelsLiveServerTestCase,
7073
):
7174
browser = "chrome"
7275
app_label = "geo"

0 commit comments

Comments
 (0)