Skip to content

Commit 667419b

Browse files
committed
[connection] Added upload method + mock-ssh-server tests
1 parent 422639e commit 667419b

File tree

8 files changed

+93
-41
lines changed

8 files changed

+93
-41
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mock = "*"
1414
pytest-django = ">=3.8.0,<4.0.0"
1515
pytest-asyncio = ">=0.10.0,<0.11.0"
1616
pytest-cov = ">=2.8.0,<2.9.0"
17+
mock-ssh-server = ">=0.8.0,<0.9.0"
1718

1819
[scripts]
1920
lint = "python -m flake8"

openwisp_controller/connection/connectors/ssh.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import logging
22
import socket
3-
from io import StringIO
3+
from io import BytesIO, StringIO
44

55
import paramiko
66
from django.utils.functional import cached_property
77
from jsonschema import validate
88
from jsonschema.exceptions import ValidationError as SchemaError
9+
from scp import SCPClient
910

1011
from .. import settings as app_settings
1112

@@ -117,13 +118,11 @@ def exec_command(self, command, timeout=app_settings.SSH_COMMAND_TIMEOUT,
117118
def update_config(self): # pragma: no cover
118119
raise NotImplementedError()
119120

120-
# TODO: this method is not used yet
121-
# but will be necessary in the future to support other OSes
122-
# def upload(self, fl, remote_path):
123-
# scp = SCPClient(self.shell.get_transport())
124-
# if not hasattr(fl, 'getvalue'):
125-
# fl_memory = BytesIO(fl.read())
126-
# fl.seek(0)
127-
# fl = fl_memory
128-
# scp.putfo(fl, remote_path)
129-
# scp.close()
121+
def upload(self, fl, remote_path):
122+
scp = SCPClient(self.shell.get_transport())
123+
if not hasattr(fl, 'getvalue'):
124+
fl_memory = BytesIO(fl.read())
125+
fl.seek(0)
126+
fl = fl_memory
127+
scp.putfo(fl, remote_path)
128+
scp.close()

openwisp_controller/connection/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ def connect(self):
211211
finally:
212212
self.last_attempt = timezone.now()
213213
self.save()
214+
return self.is_working
214215

215216
def disconnect(self):
216217
self.connector_instance.disconnect()

openwisp_controller/connection/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def update_config(device_id):
1313
"""
1414
# wait for the saving operations of this device to complete
1515
# (there may be multiple ones happening at the same time)
16-
sleep(4)
16+
sleep(2)
1717
# avoid repeating the operation multiple times
1818
device = Device.objects.select_related('config').get(pk=device_id)
1919
if device.config.status == 'applied':

openwisp_controller/connection/tests/base.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@ class CreateConnectionsMixin(CreateConfigTemplateMixin, TestOrganizationMixin):
1414
device_model = Device
1515
config_model = Config
1616

17+
_TEST_RSA_PRIVATE_KEY_PATH = os.path.join(settings.BASE_DIR, 'test-key.rsa')
18+
_TEST_RSA_PRIVATE_KEY_VALUE = None
19+
20+
class ssh_server:
21+
host = '127.0.0.1'
22+
port = 5555
23+
24+
@classmethod
25+
def setUpClass(cls):
26+
super().setUpClass()
27+
with open(cls._TEST_RSA_PRIVATE_KEY_PATH, 'r') as f:
28+
cls._TEST_RSA_PRIVATE_KEY_VALUE = f.read()
29+
30+
def _create_device(self, *args, **kwargs):
31+
if 'last_ip' not in kwargs and 'management_ip' not in kwargs:
32+
kwargs.update({
33+
'last_ip': self.ssh_server.host,
34+
'management_ip': self.ssh_server.host,
35+
})
36+
return super()._create_device(*args, **kwargs)
37+
1738
def _create_credentials(self, **kwargs):
1839
opts = dict(name='Test credentials',
1940
connector=app_settings.CONNECTORS[0][0],
@@ -31,7 +52,7 @@ def _create_credentials(self, **kwargs):
3152
def _create_credentials_with_key(self, username='root', port=22, **kwargs):
3253
opts = dict(name='Test SSH Key',
3354
params={'username': username,
34-
'key': self._SSH_PRIVATE_KEY,
55+
'key': self._TEST_RSA_PRIVATE_KEY_VALUE,
3556
'port': port})
3657
opts.update(kwargs)
3758
return self._create_credentials(**opts)
@@ -53,18 +74,3 @@ def _create_device_connection(self, **kwargs):
5374
dc.full_clean()
5475
dc.save()
5576
return dc
56-
57-
58-
class SshMixin(object):
59-
_TEST_RSA_KEY_PATH = os.path.join(settings.BASE_DIR, 'test-key.rsa')
60-
_SSH_PRIVATE_KEY = None
61-
62-
class ssh_server:
63-
host = '127.0.0.1'
64-
port = 5555
65-
66-
@classmethod
67-
def setUpClass(cls):
68-
super().setUpClass()
69-
with open(cls._TEST_RSA_KEY_PATH, 'r') as f:
70-
cls._SSH_PRIVATE_KEY = f.read()

openwisp_controller/connection/tests/test_admin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
from ...config.tests.test_admin import TestAdmin as TestConfigAdmin
66
from ...tests.utils import TestAdminMixin
77
from ..models import Credentials, DeviceConnection
8-
from .base import CreateConnectionsMixin, SshMixin
8+
from .base import CreateConnectionsMixin
99

1010

11-
class TestAdmin(TestAdminMixin, CreateConnectionsMixin,
12-
SshMixin, TestCase):
11+
class TestAdmin(TestAdminMixin, CreateConnectionsMixin, TestCase):
1312
template_model = Template
1413
credentials_model = Credentials
1514
connection_model = DeviceConnection

openwisp_controller/connection/tests/test_models.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,12 @@
99

1010
from .. import settings as app_settings
1111
from ..models import Credentials
12-
from .base import CreateConnectionsMixin, SshMixin
12+
from .base import CreateConnectionsMixin
1313

1414

15-
class TestModels(SshMixin, CreateConnectionsMixin, TestCase):
15+
class TestModels(CreateConnectionsMixin, TestCase):
1616
_connect_path = 'paramiko.SSHClient.connect'
1717

18-
def _create_device(self, *args, **kwargs):
19-
if 'last_ip' not in kwargs and 'management_ip' not in kwargs:
20-
kwargs.update({
21-
'last_ip': self.ssh_server.host,
22-
'management_ip': self.ssh_server.host,
23-
})
24-
return super()._create_device(*args, **kwargs)
25-
2618
def test_connection_str(self):
2719
c = Credentials(name='Dev Key', connector=app_settings.CONNECTORS[0][0])
2820
self.assertIn(c.name, str(c))
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import io
2+
import os
3+
from contextlib import redirect_stdout
4+
5+
import mock
6+
from django.conf import settings
7+
from django.test import TestCase
8+
from mockssh import Server
9+
10+
from .base import CreateConnectionsMixin
11+
12+
13+
class TestSsh(CreateConnectionsMixin, TestCase):
14+
@classmethod
15+
def setUpClass(cls):
16+
super().setUpClass()
17+
cls.mock_ssh_server = Server({'root': cls._TEST_RSA_PRIVATE_KEY_PATH}).__enter__()
18+
cls.ssh_server.port = cls.mock_ssh_server.port
19+
20+
@classmethod
21+
def tearDownClass(cls):
22+
cls.mock_ssh_server.__exit__()
23+
24+
def test_connection_connect(self):
25+
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
26+
dc = self._create_device_connection(credentials=ckey)
27+
dc.connector_instance.connect()
28+
stdout = io.StringIO()
29+
with redirect_stdout(stdout):
30+
dc.connector_instance.exec_command('echo test')
31+
output = stdout.getvalue()
32+
self.assertIn('$:> echo test\ntest', output)
33+
34+
def test_connection_failed_command(self):
35+
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
36+
dc = self._create_device_connection(credentials=ckey)
37+
dc.connector_instance.connect()
38+
stdout = io.StringIO()
39+
with redirect_stdout(stdout):
40+
with self.assertRaises(Exception):
41+
dc.connector_instance.exec_command('wrongcommand')
42+
output = stdout.getvalue()
43+
self.assertIn('/bin/sh: 1: wrongcommand: not found', output)
44+
self.assertIn('# Previus command failed, aborting...', output)
45+
46+
@mock.patch('scp.SCPClient.putfo')
47+
def test_connection_upload(self, putfo_mocked):
48+
ckey = self._create_credentials_with_key(port=self.ssh_server.port)
49+
dc = self._create_device_connection(credentials=ckey)
50+
dc.connector_instance.connect()
51+
# needs a binary file to test all lines
52+
fl = open(os.path.join(settings.BASE_DIR, '../media/floorplan.jpg'), 'rb')
53+
dc.connector_instance.upload(fl, '/tmp/test')
54+
putfo_mocked.assert_called_once()

0 commit comments

Comments
 (0)