arthexis 0.1.10__py3-none-any.whl → 0.1.11__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.10.dist-info → arthexis-0.1.11.dist-info}/METADATA +36 -26
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/RECORD +42 -38
- config/context_processors.py +1 -0
- config/settings.py +24 -3
- config/urls.py +5 -4
- core/admin.py +184 -22
- core/apps.py +27 -2
- core/backends.py +38 -0
- core/environment.py +23 -5
- core/mailer.py +3 -1
- core/models.py +270 -31
- core/reference_utils.py +19 -8
- core/sigil_builder.py +7 -2
- core/sigil_resolver.py +35 -4
- core/system.py +247 -1
- core/temp_passwords.py +181 -0
- core/test_system_info.py +62 -2
- core/tests.py +105 -3
- core/user_data.py +51 -8
- core/views.py +245 -8
- nodes/admin.py +137 -2
- nodes/backends.py +21 -6
- nodes/dns.py +203 -0
- nodes/models.py +293 -7
- nodes/tests.py +312 -2
- nodes/views.py +14 -0
- ocpp/consumers.py +11 -8
- ocpp/models.py +3 -0
- ocpp/reference_utils.py +42 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +30 -0
- ocpp/views.py +8 -0
- pages/admin.py +9 -1
- pages/context_processors.py +6 -6
- pages/defaults.py +14 -0
- pages/models.py +53 -14
- pages/tests.py +19 -4
- pages/urls.py +3 -0
- pages/views.py +86 -19
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.10.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
nodes/tests.py
CHANGED
|
@@ -7,6 +7,7 @@ django.setup()
|
|
|
7
7
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from types import SimpleNamespace
|
|
10
|
+
import unittest.mock as mock
|
|
10
11
|
from unittest.mock import patch, call, MagicMock
|
|
11
12
|
from django.core import mail
|
|
12
13
|
from django.core.mail import EmailMessage
|
|
@@ -21,7 +22,7 @@ import stat
|
|
|
21
22
|
import time
|
|
22
23
|
from datetime import timedelta
|
|
23
24
|
|
|
24
|
-
from django.test import Client, TestCase, TransactionTestCase, override_settings
|
|
25
|
+
from django.test import Client, SimpleTestCase, TestCase, TransactionTestCase, override_settings
|
|
25
26
|
from django.urls import reverse
|
|
26
27
|
from django.contrib.auth import get_user_model
|
|
27
28
|
from django.contrib import admin
|
|
@@ -29,9 +30,12 @@ from django.contrib.sites.models import Site
|
|
|
29
30
|
from django_celery_beat.models import PeriodicTask
|
|
30
31
|
from django.conf import settings
|
|
31
32
|
from django.utils import timezone
|
|
33
|
+
from dns import resolver as dns_resolver
|
|
32
34
|
from .actions import NodeAction
|
|
35
|
+
from . import dns as dns_utils
|
|
33
36
|
from selenium.common.exceptions import WebDriverException
|
|
34
37
|
from .utils import capture_screenshot
|
|
38
|
+
from django.db.utils import DatabaseError
|
|
35
39
|
|
|
36
40
|
from .models import (
|
|
37
41
|
Node,
|
|
@@ -41,6 +45,8 @@ from .models import (
|
|
|
41
45
|
NodeFeature,
|
|
42
46
|
NodeFeatureAssignment,
|
|
43
47
|
NetMessage,
|
|
48
|
+
NodeManager,
|
|
49
|
+
DNSRecord,
|
|
44
50
|
)
|
|
45
51
|
from .backends import OutboxEmailBackend
|
|
46
52
|
from .tasks import capture_node_screenshot, sample_clipboard
|
|
@@ -57,7 +63,23 @@ class NodeTests(TestCase):
|
|
|
57
63
|
self.client.force_login(self.user)
|
|
58
64
|
NodeRole.objects.get_or_create(name="Terminal")
|
|
59
65
|
|
|
66
|
+
|
|
67
|
+
class NodeGetLocalDatabaseUnavailableTests(SimpleTestCase):
|
|
68
|
+
def test_get_local_handles_database_errors(self):
|
|
69
|
+
with patch.object(Node.objects, "filter", side_effect=DatabaseError("fail")):
|
|
70
|
+
with self.assertLogs("nodes.models", level="DEBUG") as logs:
|
|
71
|
+
result = Node.get_local()
|
|
72
|
+
|
|
73
|
+
self.assertIsNone(result)
|
|
74
|
+
self.assertTrue(
|
|
75
|
+
any("Node.get_local skipped: database unavailable" in message for message in logs.output)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class NodeGetLocalTests(TestCase):
|
|
60
80
|
def test_register_current_does_not_create_release(self):
|
|
81
|
+
node = None
|
|
82
|
+
created = False
|
|
61
83
|
with TemporaryDirectory() as tmp:
|
|
62
84
|
base = Path(tmp)
|
|
63
85
|
with override_settings(BASE_DIR=base):
|
|
@@ -73,8 +95,11 @@ class NodeTests(TestCase):
|
|
|
73
95
|
patch("nodes.models.revision.get_revision", return_value="rev"),
|
|
74
96
|
patch.object(Node, "ensure_keys"),
|
|
75
97
|
):
|
|
76
|
-
Node.register_current()
|
|
98
|
+
node, created = Node.register_current()
|
|
77
99
|
self.assertEqual(PackageRelease.objects.count(), 0)
|
|
100
|
+
self.assertIsNotNone(node)
|
|
101
|
+
self.assertTrue(created)
|
|
102
|
+
self.assertEqual(node.current_relation, Node.Relation.SELF)
|
|
78
103
|
|
|
79
104
|
def test_register_and_list_node(self):
|
|
80
105
|
response = self.client.post(
|
|
@@ -89,6 +114,8 @@ class NodeTests(TestCase):
|
|
|
89
114
|
)
|
|
90
115
|
self.assertEqual(response.status_code, 200)
|
|
91
116
|
self.assertEqual(Node.objects.count(), 1)
|
|
117
|
+
node = Node.objects.get(mac_address="00:11:22:33:44:55")
|
|
118
|
+
self.assertEqual(node.current_relation, Node.Relation.PEER)
|
|
92
119
|
|
|
93
120
|
# allow same IP with different MAC
|
|
94
121
|
self.client.post(
|
|
@@ -371,6 +398,48 @@ class NodeTests(TestCase):
|
|
|
371
398
|
self.assertEqual(response.status_code, 200)
|
|
372
399
|
self.assertTrue(Node.objects.filter(mac_address="aa:bb:cc:dd:ee:ff").exists())
|
|
373
400
|
|
|
401
|
+
def test_register_node_respects_relation_payload(self):
|
|
402
|
+
payload = {
|
|
403
|
+
"hostname": "relation",
|
|
404
|
+
"address": "127.0.0.2",
|
|
405
|
+
"port": 8100,
|
|
406
|
+
"mac_address": "11:22:33:44:55:66",
|
|
407
|
+
"current_relation": "Downstream",
|
|
408
|
+
}
|
|
409
|
+
response = self.client.post(
|
|
410
|
+
reverse("register-node"),
|
|
411
|
+
data=json.dumps(payload),
|
|
412
|
+
content_type="application/json",
|
|
413
|
+
)
|
|
414
|
+
self.assertEqual(response.status_code, 200)
|
|
415
|
+
node = Node.objects.get(mac_address="11:22:33:44:55:66")
|
|
416
|
+
self.assertEqual(node.current_relation, Node.Relation.DOWNSTREAM)
|
|
417
|
+
|
|
418
|
+
update_payload = {
|
|
419
|
+
**payload,
|
|
420
|
+
"hostname": "relation-updated",
|
|
421
|
+
"current_relation": "Upstream",
|
|
422
|
+
}
|
|
423
|
+
second = self.client.post(
|
|
424
|
+
reverse("register-node"),
|
|
425
|
+
data=json.dumps(update_payload),
|
|
426
|
+
content_type="application/json",
|
|
427
|
+
)
|
|
428
|
+
self.assertEqual(second.status_code, 200)
|
|
429
|
+
node.refresh_from_db()
|
|
430
|
+
self.assertEqual(node.current_relation, Node.Relation.UPSTREAM)
|
|
431
|
+
|
|
432
|
+
final_payload = {**update_payload, "hostname": "relation-final"}
|
|
433
|
+
final_payload.pop("current_relation")
|
|
434
|
+
third = self.client.post(
|
|
435
|
+
reverse("register-node"),
|
|
436
|
+
data=json.dumps(final_payload),
|
|
437
|
+
content_type="application/json",
|
|
438
|
+
)
|
|
439
|
+
self.assertEqual(third.status_code, 200)
|
|
440
|
+
node.refresh_from_db()
|
|
441
|
+
self.assertEqual(node.current_relation, Node.Relation.UPSTREAM)
|
|
442
|
+
|
|
374
443
|
|
|
375
444
|
class NodeRegisterCurrentTests(TestCase):
|
|
376
445
|
def setUp(self):
|
|
@@ -1005,6 +1074,17 @@ class NetMessageReachTests(TestCase):
|
|
|
1005
1074
|
self.assertEqual(roles, {"Constellation", "Satellite", "Control"})
|
|
1006
1075
|
self.assertEqual(mock_post.call_count, 3)
|
|
1007
1076
|
|
|
1077
|
+
@patch("requests.post")
|
|
1078
|
+
def test_default_reach_not_limited_to_terminal(self, mock_post):
|
|
1079
|
+
msg = NetMessage.objects.create(subject="s", body="b")
|
|
1080
|
+
with patch.object(Node, "get_local", return_value=None), patch(
|
|
1081
|
+
"random.shuffle", side_effect=lambda seq: None
|
|
1082
|
+
):
|
|
1083
|
+
msg.propagate()
|
|
1084
|
+
roles = set(msg.propagated_to.values_list("role__name", flat=True))
|
|
1085
|
+
self.assertIn("Control", roles)
|
|
1086
|
+
self.assertEqual(mock_post.call_count, 3)
|
|
1087
|
+
|
|
1008
1088
|
|
|
1009
1089
|
class NetMessagePropagationTests(TestCase):
|
|
1010
1090
|
def setUp(self):
|
|
@@ -1034,6 +1114,7 @@ class NetMessagePropagationTests(TestCase):
|
|
|
1034
1114
|
with patch.object(Node, "get_local", return_value=self.local):
|
|
1035
1115
|
msg = NetMessage.broadcast(subject="subject", body="body")
|
|
1036
1116
|
self.assertEqual(msg.node_origin, self.local)
|
|
1117
|
+
self.assertIsNone(msg.reach)
|
|
1037
1118
|
|
|
1038
1119
|
@patch("requests.post")
|
|
1039
1120
|
@patch("core.notifications.notify")
|
|
@@ -1403,6 +1484,68 @@ class EmailOutboxTests(TestCase):
|
|
|
1403
1484
|
|
|
1404
1485
|
self.assertEqual(str(outbox), "mailer@smtp.example.com")
|
|
1405
1486
|
|
|
1487
|
+
def test_string_representation_does_not_duplicate_email_hostname(self):
|
|
1488
|
+
outbox = EmailOutbox.objects.create(
|
|
1489
|
+
host="smtp.example.com",
|
|
1490
|
+
port=587,
|
|
1491
|
+
username="mailer@example.com",
|
|
1492
|
+
password="secret",
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
self.assertEqual(str(outbox), "mailer@example.com")
|
|
1496
|
+
|
|
1497
|
+
def test_unattached_outbox_used_as_fallback(self):
|
|
1498
|
+
EmailOutbox.objects.create(
|
|
1499
|
+
group=SecurityGroup.objects.create(name="Attached"),
|
|
1500
|
+
host="smtp.attached.example.com",
|
|
1501
|
+
port=587,
|
|
1502
|
+
username="attached",
|
|
1503
|
+
password="secret",
|
|
1504
|
+
)
|
|
1505
|
+
fallback = EmailOutbox.objects.create(
|
|
1506
|
+
host="smtp.fallback.example.com",
|
|
1507
|
+
port=587,
|
|
1508
|
+
username="fallback",
|
|
1509
|
+
password="secret",
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
backend = OutboxEmailBackend()
|
|
1513
|
+
message = EmailMessage("subject", "body", to=["to@example.com"])
|
|
1514
|
+
|
|
1515
|
+
selected, fallbacks = backend._select_outbox(message)
|
|
1516
|
+
|
|
1517
|
+
self.assertEqual(selected, fallback)
|
|
1518
|
+
self.assertEqual(fallbacks, [])
|
|
1519
|
+
|
|
1520
|
+
def test_disabled_outbox_excluded_from_selection(self):
|
|
1521
|
+
EmailOutbox.objects.create(
|
|
1522
|
+
host="smtp.disabled.example.com",
|
|
1523
|
+
port=587,
|
|
1524
|
+
username="disabled@example.com",
|
|
1525
|
+
password="secret",
|
|
1526
|
+
from_email="disabled@example.com",
|
|
1527
|
+
is_enabled=False,
|
|
1528
|
+
)
|
|
1529
|
+
enabled = EmailOutbox.objects.create(
|
|
1530
|
+
host="smtp.enabled.example.com",
|
|
1531
|
+
port=587,
|
|
1532
|
+
username="enabled@example.com",
|
|
1533
|
+
password="secret",
|
|
1534
|
+
)
|
|
1535
|
+
|
|
1536
|
+
backend = OutboxEmailBackend()
|
|
1537
|
+
message = EmailMessage(
|
|
1538
|
+
"subject",
|
|
1539
|
+
"body",
|
|
1540
|
+
from_email="disabled@example.com",
|
|
1541
|
+
to=["to@example.com"],
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
selected, fallbacks = backend._select_outbox(message)
|
|
1545
|
+
|
|
1546
|
+
self.assertEqual(selected, enabled)
|
|
1547
|
+
self.assertEqual(fallbacks, [])
|
|
1548
|
+
|
|
1406
1549
|
|
|
1407
1550
|
class ClipboardTaskTests(TestCase):
|
|
1408
1551
|
@patch("nodes.tasks.pyperclip.paste")
|
|
@@ -1856,3 +1999,170 @@ class NodeRpiCameraDetectionTests(TestCase):
|
|
|
1856
1999
|
self.assertFalse(Node._has_rpi_camera())
|
|
1857
2000
|
missing_index = Node.RPI_CAMERA_BINARIES.index(Node.RPI_CAMERA_BINARIES[-1])
|
|
1858
2001
|
self.assertEqual(mock_run.call_count, missing_index)
|
|
2002
|
+
|
|
2003
|
+
|
|
2004
|
+
class DNSIntegrationTests(TestCase):
|
|
2005
|
+
def setUp(self):
|
|
2006
|
+
self.group = SecurityGroup.objects.create(name="Infra")
|
|
2007
|
+
|
|
2008
|
+
def test_deploy_records_success(self):
|
|
2009
|
+
manager = NodeManager.objects.create(
|
|
2010
|
+
group=self.group,
|
|
2011
|
+
api_key="test-key",
|
|
2012
|
+
api_secret="test-secret",
|
|
2013
|
+
)
|
|
2014
|
+
record_a = DNSRecord.objects.create(
|
|
2015
|
+
domain="example.com",
|
|
2016
|
+
name="@",
|
|
2017
|
+
record_type=DNSRecord.Type.A,
|
|
2018
|
+
data="1.2.3.4",
|
|
2019
|
+
ttl=600,
|
|
2020
|
+
)
|
|
2021
|
+
record_b = DNSRecord.objects.create(
|
|
2022
|
+
domain="example.com",
|
|
2023
|
+
name="@",
|
|
2024
|
+
record_type=DNSRecord.Type.A,
|
|
2025
|
+
data="5.6.7.8",
|
|
2026
|
+
ttl=600,
|
|
2027
|
+
)
|
|
2028
|
+
|
|
2029
|
+
calls = []
|
|
2030
|
+
|
|
2031
|
+
class DummyResponse:
|
|
2032
|
+
status_code = 200
|
|
2033
|
+
reason = "OK"
|
|
2034
|
+
|
|
2035
|
+
def json(self):
|
|
2036
|
+
return {}
|
|
2037
|
+
|
|
2038
|
+
class DummySession:
|
|
2039
|
+
def __init__(self):
|
|
2040
|
+
self.headers = {}
|
|
2041
|
+
|
|
2042
|
+
def put(self, url, json, timeout):
|
|
2043
|
+
calls.append((url, json, timeout, dict(self.headers)))
|
|
2044
|
+
return DummyResponse()
|
|
2045
|
+
|
|
2046
|
+
with mock.patch.object(dns_utils.requests, "Session", DummySession):
|
|
2047
|
+
result = manager.publish_dns_records([record_a, record_b])
|
|
2048
|
+
|
|
2049
|
+
self.assertEqual(len(result.deployed), 2)
|
|
2050
|
+
self.assertFalse(result.failures)
|
|
2051
|
+
self.assertFalse(result.skipped)
|
|
2052
|
+
self.assertTrue(calls)
|
|
2053
|
+
url, payload, timeout, headers = calls[0]
|
|
2054
|
+
self.assertTrue(url.endswith("/v1/domains/example.com/records/A/@"))
|
|
2055
|
+
self.assertEqual(len(payload), 2)
|
|
2056
|
+
self.assertEqual(headers["Authorization"], "sso-key test-key:test-secret")
|
|
2057
|
+
|
|
2058
|
+
record_a.refresh_from_db()
|
|
2059
|
+
record_b.refresh_from_db()
|
|
2060
|
+
self.assertIsNotNone(record_a.last_synced_at)
|
|
2061
|
+
self.assertIsNotNone(record_b.last_synced_at)
|
|
2062
|
+
self.assertEqual(record_a.node_manager_id, manager.pk)
|
|
2063
|
+
self.assertEqual(record_b.node_manager_id, manager.pk)
|
|
2064
|
+
|
|
2065
|
+
def test_deploy_records_handles_error(self):
|
|
2066
|
+
manager = NodeManager.objects.create(
|
|
2067
|
+
group=self.group,
|
|
2068
|
+
api_key="test-key",
|
|
2069
|
+
api_secret="test-secret",
|
|
2070
|
+
)
|
|
2071
|
+
record = DNSRecord.objects.create(
|
|
2072
|
+
domain="example.com",
|
|
2073
|
+
name="www",
|
|
2074
|
+
record_type=DNSRecord.Type.CNAME,
|
|
2075
|
+
data="target.example.com",
|
|
2076
|
+
)
|
|
2077
|
+
|
|
2078
|
+
class DummyResponse:
|
|
2079
|
+
status_code = 400
|
|
2080
|
+
reason = "Bad Request"
|
|
2081
|
+
|
|
2082
|
+
def json(self):
|
|
2083
|
+
return {"message": "Invalid data"}
|
|
2084
|
+
|
|
2085
|
+
class DummySession:
|
|
2086
|
+
def __init__(self):
|
|
2087
|
+
self.headers = {}
|
|
2088
|
+
|
|
2089
|
+
def put(self, url, json, timeout):
|
|
2090
|
+
return DummyResponse()
|
|
2091
|
+
|
|
2092
|
+
with mock.patch.object(dns_utils.requests, "Session", DummySession):
|
|
2093
|
+
result = manager.publish_dns_records([record])
|
|
2094
|
+
|
|
2095
|
+
self.assertFalse(result.deployed)
|
|
2096
|
+
self.assertIn(record, result.failures)
|
|
2097
|
+
record.refresh_from_db()
|
|
2098
|
+
self.assertEqual(record.last_error, "Invalid data")
|
|
2099
|
+
self.assertIsNone(record.last_synced_at)
|
|
2100
|
+
|
|
2101
|
+
def test_validate_record_success(self):
|
|
2102
|
+
record = DNSRecord.objects.create(
|
|
2103
|
+
domain="example.com",
|
|
2104
|
+
name="www",
|
|
2105
|
+
record_type=DNSRecord.Type.A,
|
|
2106
|
+
data="1.2.3.4",
|
|
2107
|
+
)
|
|
2108
|
+
|
|
2109
|
+
class DummyRdata:
|
|
2110
|
+
address = "1.2.3.4"
|
|
2111
|
+
|
|
2112
|
+
class DummyResolver:
|
|
2113
|
+
def resolve(self, name, rtype):
|
|
2114
|
+
self_calls.append((name, rtype))
|
|
2115
|
+
return [DummyRdata()]
|
|
2116
|
+
|
|
2117
|
+
self_calls = []
|
|
2118
|
+
ok, message = dns_utils.validate_record(record, resolver=DummyResolver())
|
|
2119
|
+
|
|
2120
|
+
self.assertTrue(ok)
|
|
2121
|
+
self.assertEqual(message, "")
|
|
2122
|
+
record.refresh_from_db()
|
|
2123
|
+
self.assertIsNotNone(record.last_verified_at)
|
|
2124
|
+
self.assertEqual(record.last_error, "")
|
|
2125
|
+
self.assertEqual(self_calls, [("www.example.com", "A")])
|
|
2126
|
+
|
|
2127
|
+
def test_validate_record_mismatch(self):
|
|
2128
|
+
record = DNSRecord.objects.create(
|
|
2129
|
+
domain="example.com",
|
|
2130
|
+
name="www",
|
|
2131
|
+
record_type=DNSRecord.Type.A,
|
|
2132
|
+
data="1.2.3.4",
|
|
2133
|
+
)
|
|
2134
|
+
|
|
2135
|
+
class DummyRdata:
|
|
2136
|
+
address = "5.6.7.8"
|
|
2137
|
+
|
|
2138
|
+
class DummyResolver:
|
|
2139
|
+
def resolve(self, name, rtype):
|
|
2140
|
+
return [DummyRdata()]
|
|
2141
|
+
|
|
2142
|
+
ok, message = dns_utils.validate_record(record, resolver=DummyResolver())
|
|
2143
|
+
|
|
2144
|
+
self.assertFalse(ok)
|
|
2145
|
+
self.assertEqual(message, "DNS record does not match expected value")
|
|
2146
|
+
record.refresh_from_db()
|
|
2147
|
+
self.assertEqual(record.last_error, message)
|
|
2148
|
+
self.assertIsNone(record.last_verified_at)
|
|
2149
|
+
|
|
2150
|
+
def test_validate_record_handles_exception(self):
|
|
2151
|
+
record = DNSRecord.objects.create(
|
|
2152
|
+
domain="example.com",
|
|
2153
|
+
name="www",
|
|
2154
|
+
record_type=DNSRecord.Type.A,
|
|
2155
|
+
data="1.2.3.4",
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
class DummyResolver:
|
|
2159
|
+
def resolve(self, name, rtype):
|
|
2160
|
+
raise dns_resolver.NXDOMAIN()
|
|
2161
|
+
|
|
2162
|
+
ok, message = dns_utils.validate_record(record, resolver=DummyResolver())
|
|
2163
|
+
|
|
2164
|
+
self.assertFalse(ok)
|
|
2165
|
+
self.assertEqual(message, "The DNS query name does not exist.")
|
|
2166
|
+
record.refresh_from_db()
|
|
2167
|
+
self.assertEqual(record.last_error, message)
|
|
2168
|
+
self.assertIsNone(record.last_verified_at)
|
nodes/views.py
CHANGED
|
@@ -204,6 +204,15 @@ def register_node(request):
|
|
|
204
204
|
signature = data.get("signature")
|
|
205
205
|
installed_version = data.get("installed_version")
|
|
206
206
|
installed_revision = data.get("installed_revision")
|
|
207
|
+
relation_present = False
|
|
208
|
+
if hasattr(data, "getlist"):
|
|
209
|
+
relation_present = "current_relation" in data
|
|
210
|
+
else:
|
|
211
|
+
relation_present = "current_relation" in data
|
|
212
|
+
raw_relation = data.get("current_relation")
|
|
213
|
+
relation_value = (
|
|
214
|
+
Node.normalize_relation(raw_relation) if relation_present else None
|
|
215
|
+
)
|
|
207
216
|
|
|
208
217
|
if not hostname or not address or not mac_address:
|
|
209
218
|
response = JsonResponse(
|
|
@@ -242,6 +251,8 @@ def register_node(request):
|
|
|
242
251
|
defaults["installed_version"] = str(installed_version)[:20]
|
|
243
252
|
if installed_revision is not None:
|
|
244
253
|
defaults["installed_revision"] = str(installed_revision)[:40]
|
|
254
|
+
if relation_value is not None:
|
|
255
|
+
defaults["current_relation"] = relation_value
|
|
245
256
|
|
|
246
257
|
node, created = Node.objects.get_or_create(
|
|
247
258
|
mac_address=mac_address,
|
|
@@ -265,6 +276,9 @@ def register_node(request):
|
|
|
265
276
|
node.installed_revision = str(installed_revision)[:40]
|
|
266
277
|
if "installed_revision" not in update_fields:
|
|
267
278
|
update_fields.append("installed_revision")
|
|
279
|
+
if relation_value is not None and node.current_relation != relation_value:
|
|
280
|
+
node.current_relation = relation_value
|
|
281
|
+
update_fields.append("current_relation")
|
|
268
282
|
node.save(update_fields=update_fields)
|
|
269
283
|
current_version = (node.installed_version or "").strip()
|
|
270
284
|
current_revision = (node.installed_revision or "").strip()
|
ocpp/consumers.py
CHANGED
|
@@ -17,6 +17,7 @@ from . import store
|
|
|
17
17
|
from decimal import Decimal
|
|
18
18
|
from django.utils.dateparse import parse_datetime
|
|
19
19
|
from .models import Transaction, Charger, MeterValue
|
|
20
|
+
from .reference_utils import host_is_local_loopback
|
|
20
21
|
|
|
21
22
|
FORWARDED_PAIR_RE = re.compile(r"for=(?:\"?)(?P<value>[^;,\"\s]+)(?:\"?)", re.IGNORECASE)
|
|
22
23
|
|
|
@@ -287,19 +288,21 @@ class CSMSConsumer(AsyncWebsocketConsumer):
|
|
|
287
288
|
serial = (self.charger_id or "").strip()
|
|
288
289
|
if not ip or not serial:
|
|
289
290
|
return
|
|
291
|
+
if host_is_local_loopback(ip):
|
|
292
|
+
return
|
|
290
293
|
host = ip
|
|
291
294
|
if ":" in host and not host.startswith("["):
|
|
292
295
|
host = f"[{host}]"
|
|
293
296
|
url = f"http://{host}:8900"
|
|
294
297
|
alt_text = f"{serial} Console"
|
|
295
|
-
reference
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
298
|
+
reference = Reference.objects.filter(alt_text=alt_text).order_by("id").first()
|
|
299
|
+
if reference is None:
|
|
300
|
+
reference = Reference.objects.create(
|
|
301
|
+
alt_text=alt_text,
|
|
302
|
+
value=url,
|
|
303
|
+
show_in_header=True,
|
|
304
|
+
method="link",
|
|
305
|
+
)
|
|
303
306
|
updated_fields: list[str] = []
|
|
304
307
|
if reference.value != url:
|
|
305
308
|
reference.value = url
|
ocpp/models.py
CHANGED
|
@@ -18,6 +18,7 @@ from core.models import (
|
|
|
18
18
|
Brand as CoreBrand,
|
|
19
19
|
EVModel as CoreEVModel,
|
|
20
20
|
)
|
|
21
|
+
from .reference_utils import url_targets_local_loopback
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class Location(Entity):
|
|
@@ -279,6 +280,8 @@ class Charger(Entity):
|
|
|
279
280
|
kwargs["update_fields"] = update_list
|
|
280
281
|
super().save(*args, **kwargs)
|
|
281
282
|
ref_value = self._full_url()
|
|
283
|
+
if url_targets_local_loopback(ref_value):
|
|
284
|
+
return
|
|
282
285
|
if not self.reference or self.reference.value != ref_value:
|
|
283
286
|
self.reference = Reference.objects.create(
|
|
284
287
|
value=ref_value, alt_text=self.charger_id
|
ocpp/reference_utils.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Helpers related to console Reference creation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipaddress
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize_host(host: str | None) -> str:
|
|
10
|
+
"""Return a trimmed host string without surrounding brackets."""
|
|
11
|
+
|
|
12
|
+
if not host:
|
|
13
|
+
return ""
|
|
14
|
+
host = host.strip()
|
|
15
|
+
if host.startswith("[") and host.endswith("]"):
|
|
16
|
+
return host[1:-1]
|
|
17
|
+
return host
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def host_is_local_loopback(host: str | None) -> bool:
|
|
21
|
+
"""Return ``True`` when the host string points to 127.0.0.1."""
|
|
22
|
+
|
|
23
|
+
normalized = _normalize_host(host)
|
|
24
|
+
if not normalized:
|
|
25
|
+
return False
|
|
26
|
+
try:
|
|
27
|
+
return ipaddress.ip_address(normalized) == ipaddress.ip_address("127.0.0.1")
|
|
28
|
+
except ValueError:
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def url_targets_local_loopback(url: str | None) -> bool:
|
|
33
|
+
"""Return ``True`` when the parsed URL host equals 127.0.0.1."""
|
|
34
|
+
|
|
35
|
+
if not url:
|
|
36
|
+
return False
|
|
37
|
+
parsed = urlparse(url)
|
|
38
|
+
return host_is_local_loopback(parsed.hostname)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
__all__ = ["host_is_local_loopback", "url_targets_local_loopback"]
|
|
42
|
+
|