arthexis 0.1.8__py3-none-any.whl → 0.1.10__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.8.dist-info → arthexis-0.1.10.dist-info}/METADATA +87 -6
- arthexis-0.1.10.dist-info/RECORD +95 -0
- arthexis-0.1.10.dist-info/licenses/LICENSE +674 -0
- config/__init__.py +0 -1
- config/auth_app.py +0 -1
- config/celery.py +1 -2
- config/context_processors.py +1 -1
- config/offline.py +2 -0
- config/settings.py +352 -37
- config/urls.py +71 -6
- core/admin.py +1601 -200
- core/admin_history.py +50 -0
- core/admindocs.py +108 -1
- core/apps.py +161 -3
- core/auto_upgrade.py +57 -0
- core/backends.py +123 -7
- core/entity.py +62 -48
- core/fields.py +98 -0
- core/github_helper.py +25 -0
- core/github_issues.py +172 -0
- core/lcd_screen.py +1 -0
- core/liveupdate.py +25 -0
- core/log_paths.py +100 -0
- core/mailer.py +83 -0
- core/middleware.py +57 -0
- core/models.py +1279 -267
- core/notifications.py +11 -1
- core/public_wifi.py +227 -0
- core/reference_utils.py +97 -0
- core/release.py +27 -20
- core/sigil_builder.py +144 -0
- core/sigil_context.py +20 -0
- core/sigil_resolver.py +284 -0
- core/system.py +162 -29
- core/tasks.py +269 -27
- core/test_system_info.py +59 -1
- core/tests.py +644 -73
- core/tests_liveupdate.py +17 -0
- core/urls.py +2 -2
- core/user_data.py +425 -168
- core/views.py +627 -59
- core/widgets.py +51 -0
- core/workgroup_urls.py +7 -3
- core/workgroup_views.py +43 -6
- nodes/actions.py +0 -2
- nodes/admin.py +168 -285
- nodes/apps.py +9 -15
- nodes/backends.py +145 -0
- nodes/lcd.py +24 -10
- nodes/models.py +579 -179
- nodes/tasks.py +1 -5
- nodes/tests.py +894 -130
- nodes/utils.py +13 -2
- nodes/views.py +204 -28
- ocpp/admin.py +212 -63
- ocpp/apps.py +1 -1
- ocpp/consumers.py +642 -68
- ocpp/evcs.py +30 -10
- ocpp/models.py +452 -70
- ocpp/simulator.py +75 -11
- ocpp/store.py +288 -30
- ocpp/tasks.py +11 -7
- ocpp/test_export_import.py +8 -7
- ocpp/test_rfid.py +211 -16
- ocpp/tests.py +1576 -137
- ocpp/transactions_io.py +68 -22
- ocpp/urls.py +35 -2
- ocpp/views.py +701 -123
- pages/admin.py +173 -13
- pages/checks.py +0 -1
- pages/context_processors.py +39 -6
- pages/forms.py +131 -0
- pages/middleware.py +153 -0
- pages/models.py +37 -9
- pages/tests.py +1182 -42
- pages/urls.py +4 -0
- pages/utils.py +0 -1
- pages/views.py +844 -51
- arthexis-0.1.8.dist-info/RECORD +0 -80
- arthexis-0.1.8.dist-info/licenses/LICENSE +0 -21
- config/workgroup_app.py +0 -7
- core/checks.py +0 -29
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/WHEEL +0 -0
- {arthexis-0.1.8.dist-info → arthexis-0.1.10.dist-info}/top_level.txt +0 -0
nodes/utils.py
CHANGED
|
@@ -14,8 +14,12 @@ SCREENSHOT_DIR = settings.LOG_DIR / "screenshots"
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def capture_screenshot(url: str) -> Path:
|
|
18
|
-
"""Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
|
|
17
|
+
def capture_screenshot(url: str, cookies=None) -> Path:
|
|
18
|
+
"""Capture a screenshot of ``url`` and save it to :data:`SCREENSHOT_DIR`.
|
|
19
|
+
|
|
20
|
+
``cookies`` can be an iterable of Selenium cookie mappings which will be
|
|
21
|
+
applied after the initial navigation and before the screenshot is taken.
|
|
22
|
+
"""
|
|
19
23
|
options = Options()
|
|
20
24
|
options.add_argument("-headless")
|
|
21
25
|
try:
|
|
@@ -27,6 +31,13 @@ def capture_screenshot(url: str) -> Path:
|
|
|
27
31
|
browser.get(url)
|
|
28
32
|
except WebDriverException as exc:
|
|
29
33
|
logger.error("Failed to load %s: %s", url, exc)
|
|
34
|
+
if cookies:
|
|
35
|
+
for cookie in cookies:
|
|
36
|
+
try:
|
|
37
|
+
browser.add_cookie(cookie)
|
|
38
|
+
except WebDriverException as exc:
|
|
39
|
+
logger.error("Failed to apply cookie for %s: %s", url, exc)
|
|
40
|
+
browser.get(url)
|
|
30
41
|
if not browser.save_screenshot(str(filename)):
|
|
31
42
|
raise RuntimeError("Screenshot capture failed")
|
|
32
43
|
return filename
|
nodes/views.py
CHANGED
|
@@ -1,34 +1,112 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import base64
|
|
2
|
+
import ipaddress
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
3
5
|
|
|
4
6
|
from django.http import JsonResponse
|
|
7
|
+
from django.http.request import split_domain_port
|
|
5
8
|
from django.views.decorators.csrf import csrf_exempt
|
|
6
9
|
from django.shortcuts import get_object_or_404
|
|
7
10
|
from django.conf import settings
|
|
8
11
|
from pathlib import Path
|
|
12
|
+
from django.utils.cache import patch_vary_headers
|
|
9
13
|
|
|
10
14
|
from utils.api import api_login_required
|
|
11
15
|
|
|
12
16
|
from cryptography.hazmat.primitives import serialization, hashes
|
|
13
17
|
from cryptography.hazmat.primitives.asymmetric import padding
|
|
14
18
|
|
|
15
|
-
from .models import Node, NetMessage, NodeRole
|
|
19
|
+
from .models import Node, NetMessage, NodeRole, node_information_updated
|
|
16
20
|
from .utils import capture_screenshot, save_screenshot
|
|
17
21
|
|
|
18
22
|
|
|
23
|
+
def _get_client_ip(request):
|
|
24
|
+
"""Return the client IP from the request headers."""
|
|
25
|
+
|
|
26
|
+
forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR", "")
|
|
27
|
+
if forwarded_for:
|
|
28
|
+
for value in forwarded_for.split(","):
|
|
29
|
+
candidate = value.strip()
|
|
30
|
+
if candidate:
|
|
31
|
+
return candidate
|
|
32
|
+
return request.META.get("REMOTE_ADDR", "")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _get_route_address(remote_ip: str, port: int) -> str:
|
|
36
|
+
"""Return the local address used to reach ``remote_ip``."""
|
|
37
|
+
|
|
38
|
+
if not remote_ip:
|
|
39
|
+
return ""
|
|
40
|
+
try:
|
|
41
|
+
parsed = ipaddress.ip_address(remote_ip)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return ""
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
target_port = int(port)
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
target_port = 1
|
|
49
|
+
if target_port <= 0 or target_port > 65535:
|
|
50
|
+
target_port = 1
|
|
51
|
+
|
|
52
|
+
family = socket.AF_INET6 if parsed.version == 6 else socket.AF_INET
|
|
53
|
+
try:
|
|
54
|
+
with socket.socket(family, socket.SOCK_DGRAM) as sock:
|
|
55
|
+
if family == socket.AF_INET6:
|
|
56
|
+
sock.connect((remote_ip, target_port, 0, 0))
|
|
57
|
+
else:
|
|
58
|
+
sock.connect((remote_ip, target_port))
|
|
59
|
+
return sock.getsockname()[0]
|
|
60
|
+
except OSError:
|
|
61
|
+
return ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_host_ip(request) -> str:
|
|
65
|
+
"""Return the IP address from the host header if available."""
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
host = request.get_host()
|
|
69
|
+
except Exception: # pragma: no cover - defensive
|
|
70
|
+
return ""
|
|
71
|
+
if not host:
|
|
72
|
+
return ""
|
|
73
|
+
domain, _ = split_domain_port(host)
|
|
74
|
+
if not domain:
|
|
75
|
+
return ""
|
|
76
|
+
try:
|
|
77
|
+
ipaddress.ip_address(domain)
|
|
78
|
+
except ValueError:
|
|
79
|
+
return ""
|
|
80
|
+
return domain
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _get_advertised_address(request, node) -> str:
|
|
84
|
+
"""Return the best address for the client to reach this node."""
|
|
85
|
+
|
|
86
|
+
client_ip = _get_client_ip(request)
|
|
87
|
+
route_address = _get_route_address(client_ip, node.port)
|
|
88
|
+
if route_address:
|
|
89
|
+
return route_address
|
|
90
|
+
host_ip = _get_host_ip(request)
|
|
91
|
+
if host_ip:
|
|
92
|
+
return host_ip
|
|
93
|
+
return node.address
|
|
94
|
+
|
|
95
|
+
|
|
19
96
|
@api_login_required
|
|
20
97
|
def node_list(request):
|
|
21
98
|
"""Return a JSON list of all known nodes."""
|
|
22
99
|
|
|
23
|
-
nodes =
|
|
24
|
-
|
|
25
|
-
"hostname",
|
|
26
|
-
"address",
|
|
27
|
-
"port",
|
|
28
|
-
"last_seen",
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
100
|
+
nodes = [
|
|
101
|
+
{
|
|
102
|
+
"hostname": node.hostname,
|
|
103
|
+
"address": node.address,
|
|
104
|
+
"port": node.port,
|
|
105
|
+
"last_seen": node.last_seen,
|
|
106
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
107
|
+
}
|
|
108
|
+
for node in Node.objects.prefetch_related("features")
|
|
109
|
+
]
|
|
32
110
|
return JsonResponse({"nodes": nodes})
|
|
33
111
|
|
|
34
112
|
|
|
@@ -41,13 +119,14 @@ def node_info(request):
|
|
|
41
119
|
node, _ = Node.register_current()
|
|
42
120
|
|
|
43
121
|
token = request.GET.get("token", "")
|
|
122
|
+
address = _get_advertised_address(request, node)
|
|
44
123
|
data = {
|
|
45
124
|
"hostname": node.hostname,
|
|
46
|
-
"address":
|
|
125
|
+
"address": address,
|
|
47
126
|
"port": node.port,
|
|
48
127
|
"mac_address": node.mac_address,
|
|
49
128
|
"public_key": node.public_key,
|
|
50
|
-
"
|
|
129
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
51
130
|
}
|
|
52
131
|
|
|
53
132
|
if token:
|
|
@@ -74,19 +153,48 @@ def node_info(request):
|
|
|
74
153
|
return response
|
|
75
154
|
|
|
76
155
|
|
|
156
|
+
def _add_cors_headers(request, response):
|
|
157
|
+
origin = request.headers.get("Origin")
|
|
158
|
+
if origin:
|
|
159
|
+
response["Access-Control-Allow-Origin"] = origin
|
|
160
|
+
response["Access-Control-Allow-Credentials"] = "true"
|
|
161
|
+
allow_headers = request.headers.get(
|
|
162
|
+
"Access-Control-Request-Headers", "Content-Type"
|
|
163
|
+
)
|
|
164
|
+
response["Access-Control-Allow-Headers"] = allow_headers
|
|
165
|
+
response["Access-Control-Allow-Methods"] = "POST, OPTIONS"
|
|
166
|
+
patch_vary_headers(response, ["Origin"])
|
|
167
|
+
return response
|
|
168
|
+
|
|
169
|
+
|
|
77
170
|
@csrf_exempt
|
|
78
|
-
@api_login_required
|
|
79
171
|
def register_node(request):
|
|
80
172
|
"""Register or update a node from POSTed JSON data."""
|
|
81
173
|
|
|
174
|
+
if request.method == "OPTIONS":
|
|
175
|
+
response = JsonResponse({"detail": "ok"})
|
|
176
|
+
return _add_cors_headers(request, response)
|
|
177
|
+
|
|
82
178
|
if request.method != "POST":
|
|
83
|
-
|
|
179
|
+
response = JsonResponse({"detail": "POST required"}, status=400)
|
|
180
|
+
return _add_cors_headers(request, response)
|
|
84
181
|
|
|
85
182
|
try:
|
|
86
183
|
data = json.loads(request.body.decode())
|
|
87
184
|
except json.JSONDecodeError:
|
|
88
185
|
data = request.POST
|
|
89
186
|
|
|
187
|
+
if hasattr(data, "getlist"):
|
|
188
|
+
raw_features = data.getlist("features")
|
|
189
|
+
if not raw_features:
|
|
190
|
+
features = None
|
|
191
|
+
elif len(raw_features) == 1:
|
|
192
|
+
features = raw_features[0]
|
|
193
|
+
else:
|
|
194
|
+
features = raw_features
|
|
195
|
+
else:
|
|
196
|
+
features = data.get("features")
|
|
197
|
+
|
|
90
198
|
hostname = data.get("hostname")
|
|
91
199
|
address = data.get("address")
|
|
92
200
|
port = data.get("port", 8000)
|
|
@@ -94,12 +202,14 @@ def register_node(request):
|
|
|
94
202
|
public_key = data.get("public_key")
|
|
95
203
|
token = data.get("token")
|
|
96
204
|
signature = data.get("signature")
|
|
97
|
-
|
|
205
|
+
installed_version = data.get("installed_version")
|
|
206
|
+
installed_revision = data.get("installed_revision")
|
|
98
207
|
|
|
99
208
|
if not hostname or not address or not mac_address:
|
|
100
|
-
|
|
209
|
+
response = JsonResponse(
|
|
101
210
|
{"detail": "hostname, address and mac_address required"}, status=400
|
|
102
211
|
)
|
|
212
|
+
return _add_cors_headers(request, response)
|
|
103
213
|
|
|
104
214
|
verified = False
|
|
105
215
|
if public_key and token and signature:
|
|
@@ -113,23 +223,33 @@ def register_node(request):
|
|
|
113
223
|
)
|
|
114
224
|
verified = True
|
|
115
225
|
except Exception:
|
|
116
|
-
|
|
226
|
+
response = JsonResponse({"detail": "invalid signature"}, status=403)
|
|
227
|
+
return _add_cors_headers(request, response)
|
|
228
|
+
|
|
229
|
+
if not verified and not request.user.is_authenticated:
|
|
230
|
+
response = JsonResponse({"detail": "authentication required"}, status=401)
|
|
231
|
+
return _add_cors_headers(request, response)
|
|
117
232
|
|
|
118
233
|
mac_address = mac_address.lower()
|
|
119
234
|
defaults = {
|
|
120
235
|
"hostname": hostname,
|
|
121
236
|
"address": address,
|
|
122
237
|
"port": port,
|
|
123
|
-
"has_lcd_screen": bool(has_lcd_screen),
|
|
124
238
|
}
|
|
125
239
|
if verified:
|
|
126
240
|
defaults["public_key"] = public_key
|
|
241
|
+
if installed_version is not None:
|
|
242
|
+
defaults["installed_version"] = str(installed_version)[:20]
|
|
243
|
+
if installed_revision is not None:
|
|
244
|
+
defaults["installed_revision"] = str(installed_revision)[:40]
|
|
127
245
|
|
|
128
246
|
node, created = Node.objects.get_or_create(
|
|
129
247
|
mac_address=mac_address,
|
|
130
248
|
defaults=defaults,
|
|
131
249
|
)
|
|
132
250
|
if not created:
|
|
251
|
+
previous_version = (node.installed_version or "").strip()
|
|
252
|
+
previous_revision = (node.installed_revision or "").strip()
|
|
133
253
|
node.hostname = hostname
|
|
134
254
|
node.address = address
|
|
135
255
|
node.port = port
|
|
@@ -137,15 +257,58 @@ def register_node(request):
|
|
|
137
257
|
if verified:
|
|
138
258
|
node.public_key = public_key
|
|
139
259
|
update_fields.append("public_key")
|
|
140
|
-
if
|
|
141
|
-
node.
|
|
142
|
-
|
|
260
|
+
if installed_version is not None:
|
|
261
|
+
node.installed_version = str(installed_version)[:20]
|
|
262
|
+
if "installed_version" not in update_fields:
|
|
263
|
+
update_fields.append("installed_version")
|
|
264
|
+
if installed_revision is not None:
|
|
265
|
+
node.installed_revision = str(installed_revision)[:40]
|
|
266
|
+
if "installed_revision" not in update_fields:
|
|
267
|
+
update_fields.append("installed_revision")
|
|
143
268
|
node.save(update_fields=update_fields)
|
|
144
|
-
|
|
269
|
+
current_version = (node.installed_version or "").strip()
|
|
270
|
+
current_revision = (node.installed_revision or "").strip()
|
|
271
|
+
node_information_updated.send(
|
|
272
|
+
sender=Node,
|
|
273
|
+
node=node,
|
|
274
|
+
previous_version=previous_version,
|
|
275
|
+
previous_revision=previous_revision,
|
|
276
|
+
current_version=current_version,
|
|
277
|
+
current_revision=current_revision,
|
|
278
|
+
request=request,
|
|
279
|
+
)
|
|
280
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
281
|
+
if isinstance(features, (str, bytes)):
|
|
282
|
+
feature_list = [features]
|
|
283
|
+
else:
|
|
284
|
+
feature_list = list(features)
|
|
285
|
+
node.update_manual_features(feature_list)
|
|
286
|
+
response = JsonResponse(
|
|
145
287
|
{"id": node.id, "detail": f"Node already exists (id: {node.id})"}
|
|
146
288
|
)
|
|
289
|
+
return _add_cors_headers(request, response)
|
|
290
|
+
|
|
291
|
+
if features is not None and (verified or request.user.is_authenticated):
|
|
292
|
+
if isinstance(features, (str, bytes)):
|
|
293
|
+
feature_list = [features]
|
|
294
|
+
else:
|
|
295
|
+
feature_list = list(features)
|
|
296
|
+
node.update_manual_features(feature_list)
|
|
297
|
+
|
|
298
|
+
current_version = (node.installed_version or "").strip()
|
|
299
|
+
current_revision = (node.installed_revision or "").strip()
|
|
300
|
+
node_information_updated.send(
|
|
301
|
+
sender=Node,
|
|
302
|
+
node=node,
|
|
303
|
+
previous_version="",
|
|
304
|
+
previous_revision="",
|
|
305
|
+
current_version=current_version,
|
|
306
|
+
current_revision=current_revision,
|
|
307
|
+
request=request,
|
|
308
|
+
)
|
|
147
309
|
|
|
148
|
-
|
|
310
|
+
response = JsonResponse({"id": node.id})
|
|
311
|
+
return _add_cors_headers(request, response)
|
|
149
312
|
|
|
150
313
|
|
|
151
314
|
@api_login_required
|
|
@@ -172,9 +335,7 @@ def public_node_endpoint(request, endpoint):
|
|
|
172
335
|
- ``POST`` broadcasts the request body as a :class:`NetMessage`.
|
|
173
336
|
"""
|
|
174
337
|
|
|
175
|
-
node = get_object_or_404(
|
|
176
|
-
Node, public_endpoint=endpoint, enable_public_api=True
|
|
177
|
-
)
|
|
338
|
+
node = get_object_or_404(Node, public_endpoint=endpoint, enable_public_api=True)
|
|
178
339
|
|
|
179
340
|
if request.method == "GET":
|
|
180
341
|
data = {
|
|
@@ -183,6 +344,7 @@ def public_node_endpoint(request, endpoint):
|
|
|
183
344
|
"port": node.port,
|
|
184
345
|
"badge_color": node.badge_color,
|
|
185
346
|
"last_seen": node.last_seen,
|
|
347
|
+
"features": list(node.features.values_list("slug", flat=True)),
|
|
186
348
|
}
|
|
187
349
|
return JsonResponse(data)
|
|
188
350
|
|
|
@@ -234,11 +396,22 @@ def net_message(request):
|
|
|
234
396
|
if reach_name:
|
|
235
397
|
reach_role = NodeRole.objects.filter(name=reach_name).first()
|
|
236
398
|
seen = data.get("seen", [])
|
|
399
|
+
origin_id = data.get("origin")
|
|
400
|
+
origin_node = None
|
|
401
|
+
if origin_id:
|
|
402
|
+
origin_node = Node.objects.filter(uuid=origin_id).first()
|
|
403
|
+
if not origin_node:
|
|
404
|
+
origin_node = node
|
|
237
405
|
if not msg_uuid:
|
|
238
406
|
return JsonResponse({"detail": "uuid required"}, status=400)
|
|
239
407
|
msg, created = NetMessage.objects.get_or_create(
|
|
240
408
|
uuid=msg_uuid,
|
|
241
|
-
defaults={
|
|
409
|
+
defaults={
|
|
410
|
+
"subject": subject[:64],
|
|
411
|
+
"body": body[:256],
|
|
412
|
+
"reach": reach_role,
|
|
413
|
+
"node_origin": origin_node,
|
|
414
|
+
},
|
|
242
415
|
)
|
|
243
416
|
if not created:
|
|
244
417
|
msg.subject = subject[:64]
|
|
@@ -247,6 +420,9 @@ def net_message(request):
|
|
|
247
420
|
if reach_role and msg.reach_id != reach_role.id:
|
|
248
421
|
msg.reach = reach_role
|
|
249
422
|
update_fields.append("reach")
|
|
423
|
+
if msg.node_origin_id is None and origin_node:
|
|
424
|
+
msg.node_origin = origin_node
|
|
425
|
+
update_fields.append("node_origin")
|
|
250
426
|
msg.save(update_fields=update_fields)
|
|
251
427
|
msg.propagate(seen=seen)
|
|
252
428
|
return JsonResponse({"status": "propagated", "complete": msg.complete})
|