arthexis 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of arthexis might be problematic. Click here for more details.
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/METADATA +39 -18
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/RECORD +31 -30
- config/settings.py +7 -0
- config/urls.py +2 -0
- core/admin.py +140 -213
- core/backends.py +3 -1
- core/models.py +612 -207
- core/system.py +67 -2
- core/tasks.py +25 -0
- core/views.py +0 -3
- nodes/admin.py +465 -292
- nodes/models.py +299 -23
- nodes/tasks.py +13 -16
- nodes/tests.py +291 -130
- nodes/urls.py +11 -0
- nodes/utils.py +9 -2
- nodes/views.py +588 -20
- ocpp/admin.py +729 -175
- ocpp/consumers.py +98 -0
- ocpp/models.py +299 -0
- ocpp/network.py +398 -0
- ocpp/tasks.py +177 -1
- ocpp/tests.py +179 -0
- ocpp/views.py +2 -0
- pages/middleware.py +3 -2
- pages/tests.py +40 -0
- pages/utils.py +70 -0
- pages/views.py +64 -32
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/WHEEL +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.23.dist-info → arthexis-0.1.25.dist-info}/top_level.txt +0 -0
nodes/tests.py
CHANGED
|
@@ -36,6 +36,7 @@ from django.test import Client, SimpleTestCase, TestCase, TransactionTestCase, o
|
|
|
36
36
|
from django.urls import reverse
|
|
37
37
|
from django.contrib.auth import get_user_model
|
|
38
38
|
from django.contrib import admin
|
|
39
|
+
from django.contrib.admin import helpers
|
|
39
40
|
from django.contrib.auth.models import Permission
|
|
40
41
|
from django_celery_beat.models import IntervalSchedule, PeriodicTask
|
|
41
42
|
from django.conf import settings
|
|
@@ -66,6 +67,7 @@ from .models import (
|
|
|
66
67
|
)
|
|
67
68
|
from .backends import OutboxEmailBackend
|
|
68
69
|
from .tasks import capture_node_screenshot, poll_unreachable_upstream, sample_clipboard
|
|
70
|
+
from ocpp.models import Charger
|
|
69
71
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
|
70
72
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
71
73
|
from core.models import Package, PackageRelease, SecurityGroup, RFID, EnergyAccount, Todo
|
|
@@ -132,6 +134,12 @@ class NodeGetLocalDatabaseUnavailableTests(SimpleTestCase):
|
|
|
132
134
|
|
|
133
135
|
|
|
134
136
|
class NodeGetLocalTests(TestCase):
|
|
137
|
+
def setUp(self):
|
|
138
|
+
super().setUp()
|
|
139
|
+
User = get_user_model()
|
|
140
|
+
self.user = User.objects.create_user(username="localtester", password="pwd")
|
|
141
|
+
self.client.force_login(self.user)
|
|
142
|
+
|
|
135
143
|
def test_normalize_relation_handles_various_inputs(self):
|
|
136
144
|
self.assertEqual(
|
|
137
145
|
Node.normalize_relation(Node.Relation.UPSTREAM),
|
|
@@ -173,6 +181,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
173
181
|
patch(
|
|
174
182
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
175
183
|
),
|
|
184
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
176
185
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
177
186
|
patch.object(Node, "ensure_keys"),
|
|
178
187
|
):
|
|
@@ -201,6 +210,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
201
210
|
patch(
|
|
202
211
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
203
212
|
),
|
|
213
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
204
214
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
205
215
|
patch.object(Node, "ensure_keys"),
|
|
206
216
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -220,6 +230,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
220
230
|
patch(
|
|
221
231
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
222
232
|
),
|
|
233
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
223
234
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
224
235
|
patch.object(Node, "ensure_keys"),
|
|
225
236
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -241,6 +252,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
241
252
|
patch(
|
|
242
253
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
243
254
|
),
|
|
255
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
244
256
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
245
257
|
patch.object(Node, "ensure_keys"),
|
|
246
258
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -259,6 +271,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
259
271
|
patch("nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:55"),
|
|
260
272
|
patch("nodes.models.socket.gethostname", return_value="localhost"),
|
|
261
273
|
patch("nodes.models.socket.gethostbyname", return_value="127.0.0.1"),
|
|
274
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
262
275
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
263
276
|
patch.object(Node, "ensure_keys"),
|
|
264
277
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -281,6 +294,7 @@ class NodeGetLocalTests(TestCase):
|
|
|
281
294
|
patch("nodes.models.Node.get_current_mac", return_value="00:11:22:33:44:56"),
|
|
282
295
|
patch("nodes.models.socket.gethostname", return_value="localhost"),
|
|
283
296
|
patch("nodes.models.socket.gethostbyname", return_value="127.0.0.1"),
|
|
297
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
284
298
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
285
299
|
patch.object(Node, "ensure_keys"),
|
|
286
300
|
patch.object(Node, "notify_peers_of_update"),
|
|
@@ -376,6 +390,37 @@ class NodeGetLocalTests(TestCase):
|
|
|
376
390
|
self.assertNotEqual(node_one.public_endpoint, node_two.public_endpoint)
|
|
377
391
|
self.assertTrue(node_two.public_endpoint.startswith("duplicate-host-"))
|
|
378
392
|
|
|
393
|
+
def test_register_node_accepts_network_hostname_without_address(self):
|
|
394
|
+
response = self.client.post(
|
|
395
|
+
reverse("register-node"),
|
|
396
|
+
data={
|
|
397
|
+
"hostname": "domain-node",
|
|
398
|
+
"network_hostname": "domain-node.example.com",
|
|
399
|
+
"port": 8050,
|
|
400
|
+
"mac_address": "aa:bb:cc:dd:ee:ff",
|
|
401
|
+
},
|
|
402
|
+
content_type="application/json",
|
|
403
|
+
)
|
|
404
|
+
self.assertEqual(response.status_code, 200)
|
|
405
|
+
node = Node.objects.get(mac_address="aa:bb:cc:dd:ee:ff")
|
|
406
|
+
self.assertEqual(node.network_hostname, "domain-node.example.com")
|
|
407
|
+
self.assertIsNone(node.address)
|
|
408
|
+
self.assertIsNone(node.ipv4_address)
|
|
409
|
+
self.assertIsNone(node.ipv6_address)
|
|
410
|
+
|
|
411
|
+
def test_register_node_requires_contact_information(self):
|
|
412
|
+
response = self.client.post(
|
|
413
|
+
reverse("register-node"),
|
|
414
|
+
data={
|
|
415
|
+
"hostname": "missing-host",
|
|
416
|
+
"port": 8051,
|
|
417
|
+
"mac_address": "aa:bb:cc:dd:ee:00",
|
|
418
|
+
},
|
|
419
|
+
content_type="application/json",
|
|
420
|
+
)
|
|
421
|
+
self.assertEqual(response.status_code, 400)
|
|
422
|
+
self.assertIn("at least one", response.json()["detail"])
|
|
423
|
+
|
|
379
424
|
def test_register_node_assigns_interface_role_and_returns_uuid(self):
|
|
380
425
|
NodeRole.objects.get_or_create(name="Interface")
|
|
381
426
|
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
@@ -805,7 +850,10 @@ class NodeInfoViewTests(TestCase):
|
|
|
805
850
|
self.addCleanup(self.patcher.stop)
|
|
806
851
|
self.node = Node.objects.create(
|
|
807
852
|
hostname="local",
|
|
853
|
+
network_hostname="local.example.com",
|
|
808
854
|
address="10.0.0.10",
|
|
855
|
+
ipv4_address="10.0.0.10",
|
|
856
|
+
ipv6_address="2001:db8::10",
|
|
809
857
|
port=8000,
|
|
810
858
|
mac_address=self.mac,
|
|
811
859
|
public_endpoint="local",
|
|
@@ -833,6 +881,8 @@ class NodeInfoViewTests(TestCase):
|
|
|
833
881
|
self.assertEqual(response.status_code, 200)
|
|
834
882
|
payload = response.json()
|
|
835
883
|
self.assertEqual(payload["port"], 80)
|
|
884
|
+
self.assertEqual(payload.get("network_hostname"), "local.example.com")
|
|
885
|
+
self.assertIn("local.example.com", payload.get("contact_hosts", []))
|
|
836
886
|
|
|
837
887
|
def test_preserves_explicit_port_in_host_header(self):
|
|
838
888
|
with self.settings(ALLOWED_HOSTS=["arthexis.com"]):
|
|
@@ -854,6 +904,8 @@ class NodeInfoViewTests(TestCase):
|
|
|
854
904
|
self.assertEqual(response.status_code, 200)
|
|
855
905
|
payload = response.json()
|
|
856
906
|
self.assertEqual(payload.get("role"), "Terminal")
|
|
907
|
+
self.assertEqual(payload.get("ipv4_address"), "10.0.0.10")
|
|
908
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:db8::10")
|
|
857
909
|
|
|
858
910
|
|
|
859
911
|
class RegisterVisitorNodeMessageTests(TestCase):
|
|
@@ -935,6 +987,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
935
987
|
patch(
|
|
936
988
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
937
989
|
),
|
|
990
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
938
991
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
939
992
|
patch.object(Node, "ensure_keys"),
|
|
940
993
|
patch.object(Node, "notify_peers_of_update") as mock_notify,
|
|
@@ -962,6 +1015,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
962
1015
|
patch(
|
|
963
1016
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
964
1017
|
),
|
|
1018
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
965
1019
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
966
1020
|
patch.object(Node, "ensure_keys"),
|
|
967
1021
|
):
|
|
@@ -980,6 +1034,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
980
1034
|
patch(
|
|
981
1035
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
982
1036
|
),
|
|
1037
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
983
1038
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
984
1039
|
patch.object(Node, "ensure_keys"),
|
|
985
1040
|
):
|
|
@@ -999,6 +1054,7 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
999
1054
|
patch(
|
|
1000
1055
|
"nodes.models.socket.gethostbyname", return_value="127.0.0.1"
|
|
1001
1056
|
),
|
|
1057
|
+
patch.object(Node, "_resolve_ip_addresses", return_value=([], [])),
|
|
1002
1058
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
1003
1059
|
patch.object(Node, "ensure_keys"),
|
|
1004
1060
|
):
|
|
@@ -1024,10 +1080,20 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1024
1080
|
return_value="00:ff:ee:dd:cc:bb",
|
|
1025
1081
|
),
|
|
1026
1082
|
patch("nodes.models.socket.gethostname", return_value="localnode"),
|
|
1083
|
+
patch("nodes.models.socket.getfqdn", return_value="localnode.example.com"),
|
|
1027
1084
|
patch(
|
|
1028
1085
|
"nodes.models.socket.gethostbyname",
|
|
1029
1086
|
return_value="192.168.1.5",
|
|
1030
1087
|
),
|
|
1088
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1089
|
+
patch.object(
|
|
1090
|
+
Node,
|
|
1091
|
+
"_resolve_ip_addresses",
|
|
1092
|
+
return_value=(
|
|
1093
|
+
["192.168.1.5", "93.184.216.34"],
|
|
1094
|
+
["fe80::1", "2001:4860:4860::8888"],
|
|
1095
|
+
),
|
|
1096
|
+
),
|
|
1031
1097
|
patch("nodes.models.revision.get_revision", return_value="newrev"),
|
|
1032
1098
|
patch("requests.post") as mock_post,
|
|
1033
1099
|
):
|
|
@@ -1051,6 +1117,9 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1051
1117
|
self.assertEqual(payload["hostname"], "localnode")
|
|
1052
1118
|
self.assertEqual(payload["installed_version"], "2.0.0")
|
|
1053
1119
|
self.assertEqual(payload["installed_revision"], "newrev")
|
|
1120
|
+
self.assertEqual(payload.get("network_hostname"), "localnode.example.com")
|
|
1121
|
+
self.assertEqual(payload.get("ipv4_address"), "93.184.216.34")
|
|
1122
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:4860:4860::8888")
|
|
1054
1123
|
|
|
1055
1124
|
def test_register_current_notifies_peers_without_version_change(self):
|
|
1056
1125
|
Node.objects.create(
|
|
@@ -1069,10 +1138,20 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1069
1138
|
return_value="00:ff:ee:dd:cc:cc",
|
|
1070
1139
|
),
|
|
1071
1140
|
patch("nodes.models.socket.gethostname", return_value="samever"),
|
|
1141
|
+
patch("nodes.models.socket.getfqdn", return_value="samever.example.com"),
|
|
1072
1142
|
patch(
|
|
1073
1143
|
"nodes.models.socket.gethostbyname",
|
|
1074
1144
|
return_value="192.168.1.6",
|
|
1075
1145
|
),
|
|
1146
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1147
|
+
patch.object(
|
|
1148
|
+
Node,
|
|
1149
|
+
"_resolve_ip_addresses",
|
|
1150
|
+
return_value=(
|
|
1151
|
+
["192.168.1.6", "93.184.216.35"],
|
|
1152
|
+
["fe80::2", "2001:4860:4860::8844"],
|
|
1153
|
+
),
|
|
1154
|
+
),
|
|
1076
1155
|
patch("nodes.models.revision.get_revision", return_value="rev1"),
|
|
1077
1156
|
patch("requests.post") as mock_post,
|
|
1078
1157
|
):
|
|
@@ -1094,6 +1173,44 @@ class NodeRegisterCurrentTests(TestCase):
|
|
|
1094
1173
|
payload = json.loads(kwargs["data"])
|
|
1095
1174
|
self.assertEqual(payload["installed_version"], "1.0.0")
|
|
1096
1175
|
self.assertEqual(payload.get("installed_revision"), "rev1")
|
|
1176
|
+
self.assertEqual(payload.get("network_hostname"), "samever.example.com")
|
|
1177
|
+
self.assertEqual(payload.get("ipv4_address"), "93.184.216.35")
|
|
1178
|
+
self.assertEqual(payload.get("ipv6_address"), "2001:4860:4860::8844")
|
|
1179
|
+
|
|
1180
|
+
def test_register_current_populates_network_fields(self):
|
|
1181
|
+
with TemporaryDirectory() as tmp:
|
|
1182
|
+
base = Path(tmp)
|
|
1183
|
+
with override_settings(BASE_DIR=base):
|
|
1184
|
+
with (
|
|
1185
|
+
patch(
|
|
1186
|
+
"nodes.models.Node.get_current_mac",
|
|
1187
|
+
return_value="00:12:34:56:78:90",
|
|
1188
|
+
),
|
|
1189
|
+
patch("nodes.models.socket.gethostname", return_value="nodehost"),
|
|
1190
|
+
patch("nodes.models.socket.getfqdn", return_value="nodehost.example.com"),
|
|
1191
|
+
patch(
|
|
1192
|
+
"nodes.models.socket.gethostbyname",
|
|
1193
|
+
return_value="10.0.0.5",
|
|
1194
|
+
),
|
|
1195
|
+
patch.dict(os.environ, {"HOSTNAME": ""}, clear=False),
|
|
1196
|
+
patch.object(
|
|
1197
|
+
Node,
|
|
1198
|
+
"_resolve_ip_addresses",
|
|
1199
|
+
return_value=(
|
|
1200
|
+
["10.0.0.5", "93.184.216.36"],
|
|
1201
|
+
["fe80::5", "2001:4860:4860::1"],
|
|
1202
|
+
),
|
|
1203
|
+
),
|
|
1204
|
+
patch("nodes.models.revision.get_revision", return_value="revX"),
|
|
1205
|
+
patch.object(Node, "ensure_keys"),
|
|
1206
|
+
patch.object(Node, "notify_peers_of_update"),
|
|
1207
|
+
):
|
|
1208
|
+
node, created = Node.register_current()
|
|
1209
|
+
self.assertTrue(created)
|
|
1210
|
+
self.assertEqual(node.network_hostname, "nodehost.example.com")
|
|
1211
|
+
self.assertEqual(node.ipv4_address, "93.184.216.36")
|
|
1212
|
+
self.assertEqual(node.ipv6_address, "2001:4860:4860::1")
|
|
1213
|
+
self.assertEqual(node.address, "93.184.216.36")
|
|
1097
1214
|
|
|
1098
1215
|
@patch("nodes.views.capture_screenshot")
|
|
1099
1216
|
def test_capture_screenshot(self, mock_capture):
|
|
@@ -1730,7 +1847,10 @@ class NodeAdminTests(TestCase):
|
|
|
1730
1847
|
|
|
1731
1848
|
payload = {
|
|
1732
1849
|
"hostname": node.hostname,
|
|
1850
|
+
"network_hostname": "remote-node.example.com",
|
|
1733
1851
|
"address": node.address,
|
|
1852
|
+
"ipv4_address": "198.51.100.10",
|
|
1853
|
+
"ipv6_address": "2001:db8::10",
|
|
1734
1854
|
"port": node.port,
|
|
1735
1855
|
"role": "Control",
|
|
1736
1856
|
}
|
|
@@ -1739,7 +1859,13 @@ class NodeAdminTests(TestCase):
|
|
|
1739
1859
|
node.refresh_from_db()
|
|
1740
1860
|
|
|
1741
1861
|
self.assertIn("role", changed)
|
|
1862
|
+
self.assertIn("network_hostname", changed)
|
|
1863
|
+
self.assertIn("ipv4_address", changed)
|
|
1864
|
+
self.assertIn("ipv6_address", changed)
|
|
1742
1865
|
self.assertEqual(node.role, control)
|
|
1866
|
+
self.assertEqual(node.network_hostname, "remote-node.example.com")
|
|
1867
|
+
self.assertEqual(node.ipv4_address, "198.51.100.10")
|
|
1868
|
+
self.assertEqual(node.ipv6_address, "2001:db8::10")
|
|
1743
1869
|
self.assertTrue(control.node_set.filter(pk=node.pk).exists())
|
|
1744
1870
|
self.assertFalse(terminal.node_set.filter(pk=node.pk).exists())
|
|
1745
1871
|
|
|
@@ -1757,7 +1883,10 @@ class NodeAdminTests(TestCase):
|
|
|
1757
1883
|
|
|
1758
1884
|
payload = {
|
|
1759
1885
|
"hostname": node.hostname,
|
|
1886
|
+
"network_hostname": "role-name-node.example.com",
|
|
1760
1887
|
"address": node.address,
|
|
1888
|
+
"ipv4_address": "198.51.100.11",
|
|
1889
|
+
"ipv6_address": "2001:db8::11",
|
|
1761
1890
|
"port": node.port,
|
|
1762
1891
|
"role_name": "Control",
|
|
1763
1892
|
}
|
|
@@ -1766,6 +1895,9 @@ class NodeAdminTests(TestCase):
|
|
|
1766
1895
|
node.refresh_from_db()
|
|
1767
1896
|
|
|
1768
1897
|
self.assertIn("role", changed)
|
|
1898
|
+
self.assertEqual(node.network_hostname, "role-name-node.example.com")
|
|
1899
|
+
self.assertEqual(node.ipv4_address, "198.51.100.11")
|
|
1900
|
+
self.assertEqual(node.ipv6_address, "2001:db8::11")
|
|
1769
1901
|
self.assertEqual(node.role, control)
|
|
1770
1902
|
self.assertTrue(control.node_set.filter(pk=node.pk).exists())
|
|
1771
1903
|
self.assertFalse(terminal.node_set.filter(pk=node.pk).exists())
|
|
@@ -1816,132 +1948,6 @@ class NodeAdminTests(TestCase):
|
|
|
1816
1948
|
self.assertEqual(response.status_code, 200)
|
|
1817
1949
|
self.assertContains(response, "data:image/png;base64")
|
|
1818
1950
|
|
|
1819
|
-
@patch("nodes.admin.requests.post")
|
|
1820
|
-
def test_proxy_view_uses_remote_login_url(self, mock_post):
|
|
1821
|
-
self.client.get(reverse("admin:nodes_node_register_current"))
|
|
1822
|
-
local_node = Node.objects.get()
|
|
1823
|
-
remote = Node.objects.create(
|
|
1824
|
-
hostname="remote",
|
|
1825
|
-
address="192.0.2.10",
|
|
1826
|
-
port=8443,
|
|
1827
|
-
mac_address="aa:bb:cc:dd:ee:ff",
|
|
1828
|
-
)
|
|
1829
|
-
mock_post.return_value = SimpleNamespace(
|
|
1830
|
-
ok=True,
|
|
1831
|
-
json=lambda: {
|
|
1832
|
-
"login_url": "https://remote.example/nodes/proxy/login/token",
|
|
1833
|
-
"expires": "2025-01-01T00:00:00",
|
|
1834
|
-
},
|
|
1835
|
-
status_code=200,
|
|
1836
|
-
text="ok",
|
|
1837
|
-
)
|
|
1838
|
-
response = self.client.get(
|
|
1839
|
-
reverse("admin:nodes_node_proxy", args=[remote.pk])
|
|
1840
|
-
)
|
|
1841
|
-
self.assertEqual(response.status_code, 200)
|
|
1842
|
-
self.assertTemplateUsed(response, "admin/nodes/node/proxy.html")
|
|
1843
|
-
self.assertContains(response, "<iframe", html=False)
|
|
1844
|
-
mock_post.assert_called()
|
|
1845
|
-
payload = json.loads(mock_post.call_args[1]["data"])
|
|
1846
|
-
self.assertEqual(payload.get("requester"), str(local_node.uuid))
|
|
1847
|
-
self.assertEqual(payload.get("requester_mac"), local_node.mac_address)
|
|
1848
|
-
self.assertEqual(payload.get("requester_public_key"), local_node.public_key)
|
|
1849
|
-
|
|
1850
|
-
@patch("nodes.admin.requests.post")
|
|
1851
|
-
def test_proxy_view_falls_back_to_http_after_ssl_error(self, mock_post):
|
|
1852
|
-
self.client.get(reverse("admin:nodes_node_register_current"))
|
|
1853
|
-
remote = Node.objects.create(
|
|
1854
|
-
hostname="remote-https",
|
|
1855
|
-
address="198.51.100.20",
|
|
1856
|
-
port=443,
|
|
1857
|
-
mac_address="aa:bb:cc:dd:ee:10",
|
|
1858
|
-
)
|
|
1859
|
-
local_node = Node.get_local()
|
|
1860
|
-
success_response = SimpleNamespace(
|
|
1861
|
-
ok=True,
|
|
1862
|
-
json=lambda: {
|
|
1863
|
-
"login_url": "http://remote.example/nodes/proxy/login/token",
|
|
1864
|
-
"expires": "2025-01-01T00:00:00",
|
|
1865
|
-
},
|
|
1866
|
-
status_code=200,
|
|
1867
|
-
text="ok",
|
|
1868
|
-
)
|
|
1869
|
-
mock_post.side_effect = [
|
|
1870
|
-
SSLError("wrong version number"),
|
|
1871
|
-
success_response,
|
|
1872
|
-
]
|
|
1873
|
-
|
|
1874
|
-
response = self.client.get(
|
|
1875
|
-
reverse("admin:nodes_node_proxy", args=[remote.pk])
|
|
1876
|
-
)
|
|
1877
|
-
|
|
1878
|
-
self.assertEqual(response.status_code, 200)
|
|
1879
|
-
self.assertEqual(mock_post.call_count, 2)
|
|
1880
|
-
first_url = mock_post.call_args_list[0].args[0]
|
|
1881
|
-
second_url = mock_post.call_args_list[1].args[0]
|
|
1882
|
-
self.assertTrue(first_url.startswith("https://"))
|
|
1883
|
-
self.assertTrue(second_url.startswith("http://"))
|
|
1884
|
-
self.assertIn("/nodes/proxy/session/", second_url)
|
|
1885
|
-
payload = json.loads(mock_post.call_args_list[-1].kwargs["data"])
|
|
1886
|
-
self.assertEqual(payload.get("requester"), str(local_node.uuid))
|
|
1887
|
-
self.assertEqual(payload.get("requester_mac"), local_node.mac_address)
|
|
1888
|
-
self.assertEqual(payload.get("requester_public_key"), local_node.public_key)
|
|
1889
|
-
|
|
1890
|
-
@patch("nodes.admin.requests.post")
|
|
1891
|
-
def test_proxy_view_retries_post_after_redirect(self, mock_post):
|
|
1892
|
-
self.client.get(reverse("admin:nodes_node_register_current"))
|
|
1893
|
-
remote = Node.objects.create(
|
|
1894
|
-
hostname="redirect-node",
|
|
1895
|
-
public_endpoint="http://remote.example",
|
|
1896
|
-
address="198.51.100.30",
|
|
1897
|
-
mac_address="aa:bb:cc:dd:ee:20",
|
|
1898
|
-
)
|
|
1899
|
-
|
|
1900
|
-
redirect_response = SimpleNamespace(
|
|
1901
|
-
status_code=301,
|
|
1902
|
-
ok=True,
|
|
1903
|
-
text="redirect",
|
|
1904
|
-
headers={"Location": "https://remote.example/nodes/proxy/session/"},
|
|
1905
|
-
)
|
|
1906
|
-
success_response = SimpleNamespace(
|
|
1907
|
-
status_code=200,
|
|
1908
|
-
ok=True,
|
|
1909
|
-
text="ok",
|
|
1910
|
-
headers={},
|
|
1911
|
-
json=lambda: {
|
|
1912
|
-
"login_url": "https://remote.example/nodes/proxy/login/token",
|
|
1913
|
-
"expires": "2025-01-01T00:00:00",
|
|
1914
|
-
},
|
|
1915
|
-
)
|
|
1916
|
-
|
|
1917
|
-
mock_post.side_effect = [redirect_response, success_response]
|
|
1918
|
-
|
|
1919
|
-
response = self.client.get(
|
|
1920
|
-
reverse("admin:nodes_node_proxy", args=[remote.pk])
|
|
1921
|
-
)
|
|
1922
|
-
|
|
1923
|
-
self.assertEqual(response.status_code, 200)
|
|
1924
|
-
self.assertEqual(mock_post.call_count, 2)
|
|
1925
|
-
|
|
1926
|
-
first_call_kwargs = mock_post.call_args_list[0].kwargs
|
|
1927
|
-
self.assertFalse(first_call_kwargs.get("allow_redirects", True))
|
|
1928
|
-
|
|
1929
|
-
second_url = mock_post.call_args_list[1].args[0]
|
|
1930
|
-
self.assertEqual(second_url, "https://remote.example/nodes/proxy/session/")
|
|
1931
|
-
second_call_kwargs = mock_post.call_args_list[1].kwargs
|
|
1932
|
-
self.assertFalse(second_call_kwargs.get("allow_redirects", True))
|
|
1933
|
-
|
|
1934
|
-
def test_proxy_link_displayed_for_remote_nodes(self):
|
|
1935
|
-
Node.objects.create(
|
|
1936
|
-
hostname="remote",
|
|
1937
|
-
address="203.0.113.1",
|
|
1938
|
-
port=8000,
|
|
1939
|
-
mac_address="aa:aa:aa:aa:aa:01",
|
|
1940
|
-
)
|
|
1941
|
-
response = self.client.get(reverse("admin:nodes_node_changelist"))
|
|
1942
|
-
proxy_url = reverse("admin:nodes_node_proxy", args=[1])
|
|
1943
|
-
self.assertContains(response, proxy_url)
|
|
1944
|
-
|
|
1945
1951
|
def test_visit_link_uses_local_admin_dashboard_for_local_node(self):
|
|
1946
1952
|
node_admin = admin.site._registry[Node]
|
|
1947
1953
|
local_node = self._create_local_node()
|
|
@@ -1974,14 +1980,14 @@ class NodeAdminTests(TestCase):
|
|
|
1974
1980
|
port=8443,
|
|
1975
1981
|
)
|
|
1976
1982
|
|
|
1977
|
-
urls = list(node_admin._iter_remote_urls(remote, "/nodes/
|
|
1983
|
+
urls = list(node_admin._iter_remote_urls(remote, "/nodes/info/"))
|
|
1978
1984
|
|
|
1979
1985
|
self.assertIn(
|
|
1980
|
-
"https://example.com:8443/interface/nodes/
|
|
1986
|
+
"https://example.com:8443/interface/nodes/info/",
|
|
1981
1987
|
urls,
|
|
1982
1988
|
)
|
|
1983
1989
|
self.assertIn(
|
|
1984
|
-
"http://example.com:8443/interface/nodes/
|
|
1990
|
+
"http://example.com:8443/interface/nodes/info/",
|
|
1985
1991
|
urls,
|
|
1986
1992
|
)
|
|
1987
1993
|
combined = "".join(urls)
|
|
@@ -2532,6 +2538,53 @@ class NodeAdminTests(TestCase):
|
|
|
2532
2538
|
)
|
|
2533
2539
|
self.assertContains(response, str(remote))
|
|
2534
2540
|
|
|
2541
|
+
def test_send_net_message_action_displays_form(self):
|
|
2542
|
+
target = Node.objects.create(
|
|
2543
|
+
hostname="remote-one", address="10.0.0.10", port=8020
|
|
2544
|
+
)
|
|
2545
|
+
response = self.client.post(
|
|
2546
|
+
reverse("admin:nodes_node_changelist"),
|
|
2547
|
+
{
|
|
2548
|
+
"action": "send_net_message",
|
|
2549
|
+
helpers.ACTION_CHECKBOX_NAME: [str(target.pk)],
|
|
2550
|
+
},
|
|
2551
|
+
follow=False,
|
|
2552
|
+
)
|
|
2553
|
+
self.assertEqual(response.status_code, 200)
|
|
2554
|
+
response.render()
|
|
2555
|
+
self.assertContains(response, "Send Net Message")
|
|
2556
|
+
self.assertContains(response, str(target))
|
|
2557
|
+
self.assertContains(response, 'name="apply"')
|
|
2558
|
+
self.assertContains(response, "Selected node (1)")
|
|
2559
|
+
|
|
2560
|
+
@patch("nodes.admin.NetMessage.propagate")
|
|
2561
|
+
def test_send_net_message_action_creates_messages(self, mock_propagate):
|
|
2562
|
+
first = Node.objects.create(
|
|
2563
|
+
hostname="remote-two", address="10.0.0.11", port=8021
|
|
2564
|
+
)
|
|
2565
|
+
second = Node.objects.create(
|
|
2566
|
+
hostname="remote-three", address="10.0.0.12", port=8022
|
|
2567
|
+
)
|
|
2568
|
+
url = reverse("admin:nodes_node_changelist")
|
|
2569
|
+
payload = {
|
|
2570
|
+
"action": "send_net_message",
|
|
2571
|
+
"apply": "1",
|
|
2572
|
+
helpers.ACTION_CHECKBOX_NAME: [str(first.pk), str(second.pk)],
|
|
2573
|
+
"subject": "Maintenance",
|
|
2574
|
+
"body": "We will reboot tonight.",
|
|
2575
|
+
}
|
|
2576
|
+
existing_ids = set(NetMessage.objects.values_list("pk", flat=True))
|
|
2577
|
+
response = self.client.post(url, payload, follow=True)
|
|
2578
|
+
self.assertEqual(response.status_code, 200)
|
|
2579
|
+
new_messages = NetMessage.objects.exclude(pk__in=existing_ids)
|
|
2580
|
+
self.assertEqual(new_messages.count(), 2)
|
|
2581
|
+
self.assertEqual(mock_propagate.call_count, 2)
|
|
2582
|
+
for node in (first, second):
|
|
2583
|
+
message = new_messages.get(filter_node=node)
|
|
2584
|
+
self.assertEqual(message.subject, "Maintenance")
|
|
2585
|
+
self.assertEqual(message.body, "We will reboot tonight.")
|
|
2586
|
+
self.assertContains(response, "Sent 2 net messages.")
|
|
2587
|
+
|
|
2535
2588
|
@patch("nodes.admin.requests.post")
|
|
2536
2589
|
@patch("nodes.admin.requests.get")
|
|
2537
2590
|
def test_update_selected_nodes_progress_updates_remote(
|
|
@@ -3189,7 +3242,7 @@ class NetMessagePropagationTests(TestCase):
|
|
|
3189
3242
|
with patch.object(Node, "get_local", return_value=self.local):
|
|
3190
3243
|
msg = NetMessage.broadcast(subject="subject", body="body")
|
|
3191
3244
|
self.assertEqual(msg.node_origin, self.local)
|
|
3192
|
-
self.
|
|
3245
|
+
self.assertEqual(msg.reach, self.role)
|
|
3193
3246
|
|
|
3194
3247
|
@patch("requests.post")
|
|
3195
3248
|
@patch("core.notifications.notify")
|
|
@@ -3605,6 +3658,82 @@ class NetMessageSignatureTests(TestCase):
|
|
|
3605
3658
|
self.assertTrue(signature_one)
|
|
3606
3659
|
self.assertTrue(signature_two)
|
|
3607
3660
|
self.assertNotEqual(signature_one, signature_two)
|
|
3661
|
+
|
|
3662
|
+
|
|
3663
|
+
class NetworkChargerActionSecurityTests(TestCase):
|
|
3664
|
+
def setUp(self):
|
|
3665
|
+
self.client = Client()
|
|
3666
|
+
self.local_node = Node.objects.create(
|
|
3667
|
+
hostname="local-node",
|
|
3668
|
+
address="127.0.0.1",
|
|
3669
|
+
port=8000,
|
|
3670
|
+
mac_address="00:aa:bb:cc:dd:10",
|
|
3671
|
+
public_endpoint="local-endpoint",
|
|
3672
|
+
)
|
|
3673
|
+
self.authorized_node = Node.objects.create(
|
|
3674
|
+
hostname="authorized-node",
|
|
3675
|
+
address="127.0.0.2",
|
|
3676
|
+
port=8001,
|
|
3677
|
+
mac_address="00:aa:bb:cc:dd:11",
|
|
3678
|
+
public_endpoint="authorized-endpoint",
|
|
3679
|
+
)
|
|
3680
|
+
self.unauthorized_node, self.unauthorized_key = self._create_signed_node(
|
|
3681
|
+
"unauthorized-node",
|
|
3682
|
+
mac_suffix=0x12,
|
|
3683
|
+
)
|
|
3684
|
+
self.charger = Charger.objects.create(
|
|
3685
|
+
charger_id="SECURE-TEST-1",
|
|
3686
|
+
allow_remote=True,
|
|
3687
|
+
manager_node=self.authorized_node,
|
|
3688
|
+
node_origin=self.local_node,
|
|
3689
|
+
)
|
|
3690
|
+
|
|
3691
|
+
def _create_signed_node(self, hostname: str, *, mac_suffix: int):
|
|
3692
|
+
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
3693
|
+
public_bytes = key.public_key().public_bytes(
|
|
3694
|
+
encoding=serialization.Encoding.PEM,
|
|
3695
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
|
3696
|
+
)
|
|
3697
|
+
node = Node.objects.create(
|
|
3698
|
+
hostname=hostname,
|
|
3699
|
+
address="10.0.0.{:d}".format(mac_suffix),
|
|
3700
|
+
port=8020,
|
|
3701
|
+
mac_address="00:aa:bb:cc:dd:{:02x}".format(mac_suffix),
|
|
3702
|
+
public_key=public_bytes.decode(),
|
|
3703
|
+
public_endpoint=f"{hostname}-endpoint",
|
|
3704
|
+
)
|
|
3705
|
+
return node, key
|
|
3706
|
+
|
|
3707
|
+
def test_rejects_requests_from_unmanaged_nodes(self):
|
|
3708
|
+
url = reverse("node-network-charger-action")
|
|
3709
|
+
payload = {
|
|
3710
|
+
"requester": str(self.unauthorized_node.uuid),
|
|
3711
|
+
"charger_id": self.charger.charger_id,
|
|
3712
|
+
"action": "reset",
|
|
3713
|
+
}
|
|
3714
|
+
body = json.dumps(payload).encode()
|
|
3715
|
+
signature = self.unauthorized_key.sign(
|
|
3716
|
+
body,
|
|
3717
|
+
padding.PKCS1v15(),
|
|
3718
|
+
hashes.SHA256(),
|
|
3719
|
+
)
|
|
3720
|
+
headers = {"HTTP_X_SIGNATURE": base64.b64encode(signature).decode()}
|
|
3721
|
+
|
|
3722
|
+
with patch.object(Node, "get_local", return_value=self.local_node):
|
|
3723
|
+
response = self.client.post(
|
|
3724
|
+
url,
|
|
3725
|
+
data=body,
|
|
3726
|
+
content_type="application/json",
|
|
3727
|
+
**headers,
|
|
3728
|
+
)
|
|
3729
|
+
|
|
3730
|
+
self.assertEqual(response.status_code, 403)
|
|
3731
|
+
self.assertEqual(
|
|
3732
|
+
response.json().get("detail"),
|
|
3733
|
+
"requester does not manage this charger",
|
|
3734
|
+
)
|
|
3735
|
+
|
|
3736
|
+
|
|
3608
3737
|
class StartupNotificationTests(TestCase):
|
|
3609
3738
|
def test_startup_notification_uses_hostname_and_revision(self):
|
|
3610
3739
|
from nodes.apps import _startup_notification
|
|
@@ -4756,6 +4885,38 @@ class ContentClassifierTests(TestCase):
|
|
|
4756
4885
|
tags = ContentClassification.objects.filter(sample=sample)
|
|
4757
4886
|
self.assertTrue(tags.filter(tag__slug="screenshot-tag").exists())
|
|
4758
4887
|
|
|
4888
|
+
def test_save_screenshot_returns_none_for_duplicate_without_linking(self):
|
|
4889
|
+
with TemporaryDirectory() as tmp:
|
|
4890
|
+
base = Path(tmp)
|
|
4891
|
+
first_path = base / "capture.png"
|
|
4892
|
+
first_path.write_bytes(b"binary image data")
|
|
4893
|
+
duplicate_path = base / "duplicate.png"
|
|
4894
|
+
duplicate_path.write_bytes(b"binary image data")
|
|
4895
|
+
with override_settings(LOG_DIR=base):
|
|
4896
|
+
original = save_screenshot(first_path, method="TEST")
|
|
4897
|
+
duplicate = save_screenshot(duplicate_path, method="TEST")
|
|
4898
|
+
|
|
4899
|
+
self.assertIsNotNone(original)
|
|
4900
|
+
self.assertIsNone(duplicate)
|
|
4901
|
+
self.assertEqual(ContentSample.objects.count(), 1)
|
|
4902
|
+
|
|
4903
|
+
def test_save_screenshot_reuses_existing_sample_when_linking(self):
|
|
4904
|
+
with TemporaryDirectory() as tmp:
|
|
4905
|
+
base = Path(tmp)
|
|
4906
|
+
first_path = base / "capture.png"
|
|
4907
|
+
first_path.write_bytes(b"binary image data")
|
|
4908
|
+
duplicate_path = base / "duplicate.png"
|
|
4909
|
+
duplicate_path.write_bytes(b"binary image data")
|
|
4910
|
+
with override_settings(LOG_DIR=base):
|
|
4911
|
+
original = save_screenshot(first_path, method="TEST")
|
|
4912
|
+
reused = save_screenshot(
|
|
4913
|
+
duplicate_path, method="TEST", link_duplicates=True
|
|
4914
|
+
)
|
|
4915
|
+
|
|
4916
|
+
self.assertIsNotNone(original)
|
|
4917
|
+
self.assertEqual(reused, original)
|
|
4918
|
+
self.assertEqual(ContentSample.objects.count(), 1)
|
|
4919
|
+
|
|
4759
4920
|
def test_text_sample_runs_default_classifiers_without_duplicates(self):
|
|
4760
4921
|
sample = ContentSample.objects.create(
|
|
4761
4922
|
content="Example content", kind=ContentSample.TEXT
|
nodes/urls.py
CHANGED
|
@@ -11,6 +11,17 @@ urlpatterns = [
|
|
|
11
11
|
path("net-message/pull/", views.net_message_pull, name="net-message-pull"),
|
|
12
12
|
path("rfid/export/", views.export_rfids, name="node-rfid-export"),
|
|
13
13
|
path("rfid/import/", views.import_rfids, name="node-rfid-import"),
|
|
14
|
+
path("network/chargers/", views.network_chargers, name="node-network-chargers"),
|
|
15
|
+
path(
|
|
16
|
+
"network/chargers/forward/",
|
|
17
|
+
views.forward_chargers,
|
|
18
|
+
name="node-network-forward-chargers",
|
|
19
|
+
),
|
|
20
|
+
path(
|
|
21
|
+
"network/chargers/action/",
|
|
22
|
+
views.network_charger_action,
|
|
23
|
+
name="node-network-charger-action",
|
|
24
|
+
),
|
|
14
25
|
path("proxy/session/", views.proxy_session, name="node-proxy-session"),
|
|
15
26
|
path("proxy/login/<str:token>/", views.proxy_login, name="node-proxy-login"),
|
|
16
27
|
path("proxy/execute/", views.proxy_execute, name="node-proxy-execute"),
|