arthexis 0.1.9__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.9.dist-info → arthexis-0.1.11.dist-info}/METADATA +76 -23
- arthexis-0.1.11.dist-info/RECORD +99 -0
- config/context_processors.py +1 -0
- config/settings.py +245 -26
- config/urls.py +11 -4
- core/admin.py +585 -57
- core/apps.py +29 -1
- core/auto_upgrade.py +57 -0
- core/backends.py +115 -3
- core/environment.py +23 -5
- core/fields.py +93 -0
- core/mailer.py +3 -1
- core/models.py +482 -38
- core/reference_utils.py +108 -0
- core/sigil_builder.py +23 -5
- core/sigil_resolver.py +35 -4
- core/system.py +400 -140
- core/tasks.py +151 -8
- core/temp_passwords.py +181 -0
- core/test_system_info.py +97 -1
- core/tests.py +393 -15
- core/user_data.py +154 -16
- core/views.py +499 -20
- nodes/admin.py +149 -6
- nodes/backends.py +125 -18
- nodes/dns.py +203 -0
- nodes/models.py +498 -9
- nodes/tests.py +682 -3
- nodes/views.py +154 -7
- ocpp/admin.py +63 -3
- ocpp/consumers.py +255 -41
- ocpp/evcs.py +6 -3
- ocpp/models.py +52 -7
- ocpp/reference_utils.py +42 -0
- ocpp/simulator.py +62 -5
- ocpp/store.py +30 -0
- ocpp/test_rfid.py +169 -7
- ocpp/tests.py +414 -8
- ocpp/views.py +109 -76
- pages/admin.py +9 -1
- pages/context_processors.py +24 -4
- pages/defaults.py +14 -0
- pages/forms.py +131 -0
- pages/models.py +53 -14
- pages/tests.py +450 -14
- pages/urls.py +4 -0
- pages/views.py +419 -110
- arthexis-0.1.9.dist-info/RECORD +0 -92
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/WHEEL +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.9.dist-info → arthexis-0.1.11.dist-info}/top_level.txt +0 -0
core/system.py
CHANGED
|
@@ -1,23 +1,330 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from contextlib import closing
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from datetime import datetime
|
|
3
6
|
from pathlib import Path
|
|
4
|
-
import
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
5
9
|
import socket
|
|
6
10
|
import subprocess
|
|
7
11
|
import shutil
|
|
8
|
-
import
|
|
9
|
-
import time
|
|
12
|
+
from typing import Callable, Iterable, Optional
|
|
10
13
|
|
|
11
|
-
from django import forms
|
|
12
14
|
from django.conf import settings
|
|
13
|
-
from django.contrib import admin
|
|
14
|
-
from django.core.management import get_commands, load_command_class
|
|
15
|
-
from django.http import Http404
|
|
16
|
-
from django.shortcuts import redirect
|
|
15
|
+
from django.contrib import admin
|
|
17
16
|
from django.template.response import TemplateResponse
|
|
18
|
-
from django.urls import path
|
|
17
|
+
from django.urls import path
|
|
18
|
+
from django.utils import timezone
|
|
19
|
+
from django.utils.formats import date_format
|
|
19
20
|
from django.utils.translation import gettext_lazy as _
|
|
20
21
|
|
|
22
|
+
from core.auto_upgrade import AUTO_UPGRADE_TASK_NAME
|
|
23
|
+
from utils import revision
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SystemField:
|
|
28
|
+
"""Metadata describing a single entry on the system admin page."""
|
|
29
|
+
|
|
30
|
+
label: str
|
|
31
|
+
sigil_key: str
|
|
32
|
+
value: object
|
|
33
|
+
field_type: str = "text"
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def sigil(self) -> str:
|
|
37
|
+
return f"SYS.{self.sigil_key}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_RUNSERVER_PORT_PATTERN = re.compile(r":(\d{2,5})(?:\D|$)")
|
|
41
|
+
_RUNSERVER_PORT_FLAG_PATTERN = re.compile(r"--port(?:=|\s+)(\d{2,5})", re.IGNORECASE)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _format_timestamp(dt: datetime | None) -> str:
|
|
45
|
+
"""Return ``dt`` formatted using the active ``DATETIME_FORMAT``."""
|
|
46
|
+
|
|
47
|
+
if dt is None:
|
|
48
|
+
return ""
|
|
49
|
+
try:
|
|
50
|
+
localized = timezone.localtime(dt)
|
|
51
|
+
except Exception:
|
|
52
|
+
localized = dt
|
|
53
|
+
return date_format(localized, "DATETIME_FORMAT")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _auto_upgrade_next_check() -> str:
|
|
57
|
+
"""Return the human-readable timestamp for the next auto-upgrade check."""
|
|
58
|
+
|
|
59
|
+
try: # pragma: no cover - optional dependency failures
|
|
60
|
+
from django_celery_beat.models import PeriodicTask
|
|
61
|
+
except Exception:
|
|
62
|
+
return ""
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
task = (
|
|
66
|
+
PeriodicTask.objects.select_related(
|
|
67
|
+
"interval", "crontab", "solar", "clocked"
|
|
68
|
+
)
|
|
69
|
+
.only("enabled", "last_run_at", "start_time", "name")
|
|
70
|
+
.get(name=AUTO_UPGRADE_TASK_NAME)
|
|
71
|
+
)
|
|
72
|
+
except PeriodicTask.DoesNotExist:
|
|
73
|
+
return ""
|
|
74
|
+
except Exception: # pragma: no cover - database unavailable
|
|
75
|
+
return ""
|
|
76
|
+
|
|
77
|
+
if not task.enabled:
|
|
78
|
+
return str(_("Disabled"))
|
|
79
|
+
|
|
80
|
+
schedule = task.schedule
|
|
81
|
+
if schedule is None:
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
now = schedule.maybe_make_aware(schedule.now())
|
|
85
|
+
|
|
86
|
+
start_time = task.start_time
|
|
87
|
+
if start_time is not None:
|
|
88
|
+
try:
|
|
89
|
+
candidate_start = schedule.maybe_make_aware(start_time)
|
|
90
|
+
except Exception:
|
|
91
|
+
candidate_start = (
|
|
92
|
+
timezone.make_aware(start_time)
|
|
93
|
+
if timezone.is_naive(start_time)
|
|
94
|
+
else start_time
|
|
95
|
+
)
|
|
96
|
+
if candidate_start and candidate_start > now:
|
|
97
|
+
return _format_timestamp(candidate_start)
|
|
98
|
+
|
|
99
|
+
last_run_at = task.last_run_at
|
|
100
|
+
if last_run_at is not None:
|
|
101
|
+
try:
|
|
102
|
+
reference = schedule.maybe_make_aware(last_run_at)
|
|
103
|
+
except Exception:
|
|
104
|
+
reference = (
|
|
105
|
+
timezone.make_aware(last_run_at)
|
|
106
|
+
if timezone.is_naive(last_run_at)
|
|
107
|
+
else last_run_at
|
|
108
|
+
)
|
|
109
|
+
else:
|
|
110
|
+
reference = now
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
remaining = schedule.remaining_estimate(reference)
|
|
114
|
+
except Exception:
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
next_run = now + remaining
|
|
118
|
+
return _format_timestamp(next_run)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_auto_upgrade_namespace(key: str) -> str | None:
|
|
122
|
+
"""Resolve sigils within the ``AUTO-UPGRADE`` namespace."""
|
|
123
|
+
|
|
124
|
+
normalized = key.replace("-", "_").upper()
|
|
125
|
+
if normalized == "NEXT_CHECK":
|
|
126
|
+
return _auto_upgrade_next_check()
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
_SYSTEM_SIGIL_NAMESPACES: dict[str, Callable[[str], Optional[str]]] = {
|
|
131
|
+
"AUTO_UPGRADE": _resolve_auto_upgrade_namespace,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolve_system_namespace_value(key: str) -> str | None:
|
|
136
|
+
"""Resolve dot-notation sigils mapped to dynamic ``SYS`` namespaces."""
|
|
137
|
+
|
|
138
|
+
if not key:
|
|
139
|
+
return None
|
|
140
|
+
namespace, _, remainder = key.partition(".")
|
|
141
|
+
if not remainder:
|
|
142
|
+
return None
|
|
143
|
+
normalized = namespace.replace("-", "_").upper()
|
|
144
|
+
handler = _SYSTEM_SIGIL_NAMESPACES.get(normalized)
|
|
145
|
+
if not handler:
|
|
146
|
+
return None
|
|
147
|
+
return handler(remainder)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _database_configurations() -> list[dict[str, str]]:
|
|
151
|
+
"""Return a normalized list of configured database connections."""
|
|
152
|
+
|
|
153
|
+
databases: list[dict[str, str]] = []
|
|
154
|
+
for alias, config in settings.DATABASES.items():
|
|
155
|
+
engine = config.get("ENGINE", "")
|
|
156
|
+
name = config.get("NAME", "")
|
|
157
|
+
if engine is None:
|
|
158
|
+
engine = ""
|
|
159
|
+
if name is None:
|
|
160
|
+
name = ""
|
|
161
|
+
databases.append({
|
|
162
|
+
"alias": alias,
|
|
163
|
+
"engine": str(engine),
|
|
164
|
+
"name": str(name),
|
|
165
|
+
})
|
|
166
|
+
databases.sort(key=lambda entry: entry["alias"].lower())
|
|
167
|
+
return databases
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _build_system_fields(info: dict[str, object]) -> list[SystemField]:
|
|
171
|
+
"""Convert gathered system information into renderable rows."""
|
|
172
|
+
|
|
173
|
+
fields: list[SystemField] = []
|
|
174
|
+
|
|
175
|
+
def add_field(label: str, key: str, value: object, *, field_type: str = "text", visible: bool = True) -> None:
|
|
176
|
+
if not visible:
|
|
177
|
+
return
|
|
178
|
+
fields.append(SystemField(label=label, sigil_key=key, value=value, field_type=field_type))
|
|
179
|
+
|
|
180
|
+
add_field(_("Suite installed"), "INSTALLED", info.get("installed", False), field_type="boolean")
|
|
181
|
+
add_field(_("Revision"), "REVISION", info.get("revision", ""))
|
|
182
|
+
|
|
183
|
+
service_value = info.get("service") or _("not installed")
|
|
184
|
+
add_field(_("Service"), "SERVICE", service_value)
|
|
185
|
+
|
|
186
|
+
nginx_mode = info.get("mode", "")
|
|
187
|
+
port = info.get("port", "")
|
|
188
|
+
nginx_display = f"{nginx_mode} ({port})" if port else nginx_mode
|
|
189
|
+
add_field(_("Nginx mode"), "NGINX_MODE", nginx_display)
|
|
190
|
+
|
|
191
|
+
add_field(_("Node role"), "NODE_ROLE", info.get("role", ""))
|
|
192
|
+
add_field(
|
|
193
|
+
_("Display mode"),
|
|
194
|
+
"DISPLAY_MODE",
|
|
195
|
+
info.get("screen_mode", ""),
|
|
196
|
+
visible=bool(info.get("screen_mode")),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
add_field(_("Features"), "FEATURES", info.get("features", []), field_type="features")
|
|
200
|
+
add_field(_("Running"), "RUNNING", info.get("running", False), field_type="boolean")
|
|
201
|
+
add_field(
|
|
202
|
+
_("Service status"),
|
|
203
|
+
"SERVICE_STATUS",
|
|
204
|
+
info.get("service_status", ""),
|
|
205
|
+
visible=bool(info.get("service")),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
add_field(_("Hostname"), "HOSTNAME", info.get("hostname", ""))
|
|
209
|
+
|
|
210
|
+
ip_addresses: Iterable[str] = info.get("ip_addresses", []) # type: ignore[assignment]
|
|
211
|
+
add_field(_("IP addresses"), "IP_ADDRESSES", " ".join(ip_addresses))
|
|
212
|
+
|
|
213
|
+
add_field(
|
|
214
|
+
_("Databases"),
|
|
215
|
+
"DATABASES",
|
|
216
|
+
info.get("databases", []),
|
|
217
|
+
field_type="databases",
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
add_field(
|
|
221
|
+
_("Next auto-upgrade check"),
|
|
222
|
+
"AUTO-UPGRADE.NEXT-CHECK",
|
|
223
|
+
info.get("auto_upgrade_next_check", ""),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return fields
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _export_field_value(field: SystemField) -> str:
|
|
230
|
+
"""Serialize a ``SystemField`` value for sigil resolution."""
|
|
231
|
+
|
|
232
|
+
if field.field_type in {"features", "databases"}:
|
|
233
|
+
return json.dumps(field.value)
|
|
234
|
+
if field.field_type == "boolean":
|
|
235
|
+
return "True" if field.value else "False"
|
|
236
|
+
if field.value is None:
|
|
237
|
+
return ""
|
|
238
|
+
return str(field.value)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_system_sigil_values() -> dict[str, str]:
|
|
242
|
+
"""Expose system information in a format suitable for sigil lookups."""
|
|
243
|
+
|
|
244
|
+
info = _gather_info()
|
|
245
|
+
values: dict[str, str] = {}
|
|
246
|
+
for field in _build_system_fields(info):
|
|
247
|
+
exported = _export_field_value(field)
|
|
248
|
+
raw_key = (field.sigil_key or "").strip()
|
|
249
|
+
if not raw_key:
|
|
250
|
+
continue
|
|
251
|
+
variants = {
|
|
252
|
+
raw_key.upper(),
|
|
253
|
+
raw_key.replace("-", "_").upper(),
|
|
254
|
+
}
|
|
255
|
+
for variant in variants:
|
|
256
|
+
values[variant] = exported
|
|
257
|
+
return values
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _parse_runserver_port(command_line: str) -> int | None:
|
|
261
|
+
"""Extract the HTTP port from a runserver command line."""
|
|
262
|
+
|
|
263
|
+
for pattern in (_RUNSERVER_PORT_PATTERN, _RUNSERVER_PORT_FLAG_PATTERN):
|
|
264
|
+
match = pattern.search(command_line)
|
|
265
|
+
if match:
|
|
266
|
+
try:
|
|
267
|
+
return int(match.group(1))
|
|
268
|
+
except ValueError:
|
|
269
|
+
continue
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _detect_runserver_process() -> tuple[bool, int | None]:
|
|
274
|
+
"""Return whether the dev server is running and the port if available."""
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
result = subprocess.run(
|
|
278
|
+
["pgrep", "-af", "manage.py runserver"],
|
|
279
|
+
capture_output=True,
|
|
280
|
+
text=True,
|
|
281
|
+
check=False,
|
|
282
|
+
)
|
|
283
|
+
except FileNotFoundError:
|
|
284
|
+
return False, None
|
|
285
|
+
except Exception:
|
|
286
|
+
return False, None
|
|
287
|
+
|
|
288
|
+
if result.returncode != 0:
|
|
289
|
+
return False, None
|
|
290
|
+
|
|
291
|
+
output = result.stdout.strip()
|
|
292
|
+
if not output:
|
|
293
|
+
return False, None
|
|
294
|
+
|
|
295
|
+
port = None
|
|
296
|
+
for line in output.splitlines():
|
|
297
|
+
port = _parse_runserver_port(line)
|
|
298
|
+
if port is not None:
|
|
299
|
+
break
|
|
300
|
+
|
|
301
|
+
if port is None:
|
|
302
|
+
port = 8000
|
|
303
|
+
|
|
304
|
+
return True, port
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _probe_ports(candidates: list[int]) -> tuple[bool, int | None]:
|
|
308
|
+
"""Attempt to connect to localhost on the provided ports."""
|
|
309
|
+
|
|
310
|
+
for port in candidates:
|
|
311
|
+
try:
|
|
312
|
+
with closing(socket.create_connection(("localhost", port), timeout=0.25)):
|
|
313
|
+
return True, port
|
|
314
|
+
except OSError:
|
|
315
|
+
continue
|
|
316
|
+
return False, None
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _port_candidates(default_port: int) -> list[int]:
|
|
320
|
+
"""Return a prioritized list of ports to probe for the HTTP service."""
|
|
321
|
+
|
|
322
|
+
candidates = [default_port]
|
|
323
|
+
for port in (8000, 8888):
|
|
324
|
+
if port not in candidates:
|
|
325
|
+
candidates.append(port)
|
|
326
|
+
return candidates
|
|
327
|
+
|
|
21
328
|
|
|
22
329
|
def _gather_info() -> dict:
|
|
23
330
|
"""Collect basic system information similar to status.sh."""
|
|
@@ -26,6 +333,7 @@ def _gather_info() -> dict:
|
|
|
26
333
|
info: dict[str, object] = {}
|
|
27
334
|
|
|
28
335
|
info["installed"] = (base_dir / ".venv").exists()
|
|
336
|
+
info["revision"] = revision.get_revision()
|
|
29
337
|
|
|
30
338
|
service_file = lock_dir / "service.lck"
|
|
31
339
|
info["service"] = service_file.read_text().strip() if service_file.exists() else ""
|
|
@@ -33,7 +341,8 @@ def _gather_info() -> dict:
|
|
|
33
341
|
mode_file = lock_dir / "nginx_mode.lck"
|
|
34
342
|
mode = mode_file.read_text().strip() if mode_file.exists() else "internal"
|
|
35
343
|
info["mode"] = mode
|
|
36
|
-
|
|
344
|
+
default_port = 8000 if mode == "public" else 8888
|
|
345
|
+
detected_port: int | None = None
|
|
37
346
|
|
|
38
347
|
screen_file = lock_dir / "screen_mode.lck"
|
|
39
348
|
info["screen_mode"] = (
|
|
@@ -43,11 +352,68 @@ def _gather_info() -> dict:
|
|
|
43
352
|
# Use settings.NODE_ROLE as the single source of truth for the node role.
|
|
44
353
|
info["role"] = getattr(settings, "NODE_ROLE", "Terminal")
|
|
45
354
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
355
|
+
features: list[dict[str, object]] = []
|
|
356
|
+
try:
|
|
357
|
+
from nodes.models import Node, NodeFeature
|
|
358
|
+
except Exception:
|
|
359
|
+
info["features"] = features
|
|
360
|
+
else:
|
|
361
|
+
feature_map: dict[str, dict[str, object]] = {}
|
|
362
|
+
|
|
363
|
+
def _add_feature(feature: NodeFeature, flag: str) -> None:
|
|
364
|
+
slug = getattr(feature, "slug", "") or ""
|
|
365
|
+
if not slug:
|
|
366
|
+
return
|
|
367
|
+
display = (getattr(feature, "display", "") or "").strip()
|
|
368
|
+
normalized = display or slug.replace("-", " ").title()
|
|
369
|
+
entry = feature_map.setdefault(
|
|
370
|
+
slug,
|
|
371
|
+
{
|
|
372
|
+
"slug": slug,
|
|
373
|
+
"display": normalized,
|
|
374
|
+
"expected": False,
|
|
375
|
+
"actual": False,
|
|
376
|
+
},
|
|
377
|
+
)
|
|
378
|
+
if display:
|
|
379
|
+
entry["display"] = display
|
|
380
|
+
entry[flag] = True
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
expected_features = (
|
|
384
|
+
NodeFeature.objects.filter(roles__name=info["role"]).only("slug", "display").distinct()
|
|
385
|
+
)
|
|
386
|
+
except Exception:
|
|
387
|
+
expected_features = []
|
|
388
|
+
try:
|
|
389
|
+
for feature in expected_features:
|
|
390
|
+
_add_feature(feature, "expected")
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
local_node = Node.get_local()
|
|
396
|
+
except Exception:
|
|
397
|
+
local_node = None
|
|
398
|
+
|
|
399
|
+
actual_features = []
|
|
400
|
+
if local_node:
|
|
401
|
+
try:
|
|
402
|
+
actual_features = list(local_node.features.only("slug", "display"))
|
|
403
|
+
except Exception:
|
|
404
|
+
actual_features = []
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
for feature in actual_features:
|
|
408
|
+
_add_feature(feature, "actual")
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
features = sorted(
|
|
413
|
+
feature_map.values(),
|
|
414
|
+
key=lambda item: str(item.get("display", "")).lower(),
|
|
415
|
+
)
|
|
416
|
+
info["features"] = features
|
|
51
417
|
|
|
52
418
|
running = False
|
|
53
419
|
service_status = ""
|
|
@@ -65,17 +431,20 @@ def _gather_info() -> dict:
|
|
|
65
431
|
except Exception:
|
|
66
432
|
pass
|
|
67
433
|
else:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
["pgrep", "-f", "manage.py runserver"],
|
|
71
|
-
check=True,
|
|
72
|
-
stdout=subprocess.PIPE,
|
|
73
|
-
stderr=subprocess.PIPE,
|
|
74
|
-
)
|
|
434
|
+
process_running, process_port = _detect_runserver_process()
|
|
435
|
+
if process_running:
|
|
75
436
|
running = True
|
|
76
|
-
|
|
77
|
-
|
|
437
|
+
detected_port = process_port
|
|
438
|
+
|
|
439
|
+
if not running or detected_port is None:
|
|
440
|
+
probe_running, probe_port = _probe_ports(_port_candidates(default_port))
|
|
441
|
+
if probe_running:
|
|
442
|
+
running = True
|
|
443
|
+
if detected_port is None:
|
|
444
|
+
detected_port = probe_port
|
|
445
|
+
|
|
78
446
|
info["running"] = running
|
|
447
|
+
info["port"] = detected_port if detected_port is not None else default_port
|
|
79
448
|
info["service_status"] = service_status
|
|
80
449
|
|
|
81
450
|
try:
|
|
@@ -87,128 +456,24 @@ def _gather_info() -> dict:
|
|
|
87
456
|
info["hostname"] = hostname
|
|
88
457
|
info["ip_addresses"] = ip_list
|
|
89
458
|
|
|
459
|
+
info["databases"] = _database_configurations()
|
|
460
|
+
info["auto_upgrade_next_check"] = _auto_upgrade_next_check()
|
|
461
|
+
|
|
90
462
|
return info
|
|
91
463
|
|
|
92
464
|
|
|
93
465
|
def _system_view(request):
|
|
94
466
|
info = _gather_info()
|
|
95
|
-
if request.method == "POST" and request.user.is_superuser:
|
|
96
|
-
action = request.POST.get("action")
|
|
97
|
-
stop_script = Path(settings.BASE_DIR) / "stop.sh"
|
|
98
|
-
args = [str(stop_script)]
|
|
99
|
-
if action == "stop":
|
|
100
|
-
password = request.POST.get("password", "")
|
|
101
|
-
if not request.user.check_password(password):
|
|
102
|
-
messages.error(request, _("Incorrect password."))
|
|
103
|
-
else:
|
|
104
|
-
lock_file = Path(settings.BASE_DIR) / "locks" / "charging.lck"
|
|
105
|
-
age = None
|
|
106
|
-
if lock_file.exists():
|
|
107
|
-
age = time.time() - lock_file.stat().st_mtime
|
|
108
|
-
if lock_file.exists() and age is not None and age <= 600:
|
|
109
|
-
messages.error(request, _("Charging session in progress."))
|
|
110
|
-
else:
|
|
111
|
-
if info["service"]:
|
|
112
|
-
args.append("--all")
|
|
113
|
-
subprocess.Popen(args)
|
|
114
|
-
return redirect(reverse("admin:index"))
|
|
115
|
-
elif action == "restart":
|
|
116
|
-
subprocess.Popen(args)
|
|
117
|
-
return redirect(reverse("admin:index"))
|
|
118
|
-
|
|
119
|
-
excluded = {
|
|
120
|
-
"shell",
|
|
121
|
-
"dbshell",
|
|
122
|
-
"createsuperuser",
|
|
123
|
-
"changepassword",
|
|
124
|
-
"startapp",
|
|
125
|
-
"startproject",
|
|
126
|
-
"runserver",
|
|
127
|
-
}
|
|
128
|
-
commands = sorted(cmd for cmd in get_commands().keys() if cmd not in excluded)
|
|
129
|
-
|
|
130
|
-
context = admin.site.each_context(request)
|
|
131
|
-
context.update({"title": _("System"), "info": info, "commands": commands})
|
|
132
|
-
return TemplateResponse(request, "admin/system.html", context)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _build_form(parser: argparse.ArgumentParser) -> type[forms.Form]:
|
|
136
|
-
fields: dict[str, forms.Field] = {}
|
|
137
|
-
for action in parser._actions:
|
|
138
|
-
if action.help == argparse.SUPPRESS or action.dest == "help":
|
|
139
|
-
continue
|
|
140
|
-
label = action.option_strings[0] if action.option_strings else action.dest
|
|
141
|
-
required = (
|
|
142
|
-
action.required
|
|
143
|
-
if action.option_strings
|
|
144
|
-
else action.nargs not in ["?", "*", argparse.OPTIONAL]
|
|
145
|
-
)
|
|
146
|
-
fields[action.dest] = forms.CharField(label=label, required=required)
|
|
147
|
-
return type("CommandForm", (forms.Form,), fields)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def _system_command_view(request, command):
|
|
151
|
-
commands = get_commands()
|
|
152
|
-
if command not in commands:
|
|
153
|
-
raise Http404
|
|
154
|
-
app_name = commands[command]
|
|
155
|
-
cmd_instance = load_command_class(app_name, command)
|
|
156
|
-
parser = cmd_instance.create_parser("manage.py", command)
|
|
157
|
-
form_class = _build_form(parser)
|
|
158
|
-
form = form_class(request.POST or None)
|
|
159
|
-
output = ""
|
|
160
|
-
|
|
161
|
-
has_required = any(
|
|
162
|
-
(a.option_strings and a.required)
|
|
163
|
-
or (not a.option_strings and a.nargs not in ["?", "*", argparse.OPTIONAL])
|
|
164
|
-
for a in parser._actions
|
|
165
|
-
if a.help != argparse.SUPPRESS and a.dest != "help"
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
if not has_required and request.method == "GET":
|
|
169
|
-
out = io.StringIO()
|
|
170
|
-
cmd_instance.stdout = out
|
|
171
|
-
cmd_instance.stderr = out
|
|
172
|
-
try:
|
|
173
|
-
cmd_instance.run_from_argv(["manage.py", command])
|
|
174
|
-
except Exception as exc:
|
|
175
|
-
out.write(str(exc))
|
|
176
|
-
output = out.getvalue()
|
|
177
|
-
form = None
|
|
178
|
-
elif request.method == "POST" and form.is_valid():
|
|
179
|
-
argv = ["manage.py", command]
|
|
180
|
-
for action in parser._actions:
|
|
181
|
-
if action.help == argparse.SUPPRESS or action.dest == "help":
|
|
182
|
-
continue
|
|
183
|
-
val = form.cleaned_data.get(action.dest)
|
|
184
|
-
if val in (None, ""):
|
|
185
|
-
continue
|
|
186
|
-
if action.option_strings:
|
|
187
|
-
argv.append(action.option_strings[0])
|
|
188
|
-
if action.nargs != 0:
|
|
189
|
-
argv.append(val)
|
|
190
|
-
else:
|
|
191
|
-
argv.append(val)
|
|
192
|
-
out = io.StringIO()
|
|
193
|
-
cmd_instance.stdout = out
|
|
194
|
-
cmd_instance.stderr = out
|
|
195
|
-
try:
|
|
196
|
-
cmd_instance.run_from_argv(argv)
|
|
197
|
-
except Exception as exc:
|
|
198
|
-
out.write(str(exc))
|
|
199
|
-
output = out.getvalue()
|
|
200
|
-
form = None
|
|
201
467
|
|
|
202
468
|
context = admin.site.each_context(request)
|
|
203
469
|
context.update(
|
|
204
470
|
{
|
|
205
|
-
"title":
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"output": output,
|
|
471
|
+
"title": _("System"),
|
|
472
|
+
"info": info,
|
|
473
|
+
"system_fields": _build_system_fields(info),
|
|
209
474
|
}
|
|
210
475
|
)
|
|
211
|
-
return TemplateResponse(request, "admin/
|
|
476
|
+
return TemplateResponse(request, "admin/system.html", context)
|
|
212
477
|
|
|
213
478
|
|
|
214
479
|
def patch_admin_system_view() -> None:
|
|
@@ -219,11 +484,6 @@ def patch_admin_system_view() -> None:
|
|
|
219
484
|
urls = original_get_urls()
|
|
220
485
|
custom = [
|
|
221
486
|
path("system/", admin.site.admin_view(_system_view), name="system"),
|
|
222
|
-
path(
|
|
223
|
-
"system/command/<str:command>/",
|
|
224
|
-
admin.site.admin_view(_system_command_view),
|
|
225
|
-
name="system_command",
|
|
226
|
-
),
|
|
227
487
|
]
|
|
228
488
|
return custom + urls
|
|
229
489
|
|