arthexis 0.1.22__py3-none-any.whl → 0.1.23__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.22.dist-info → arthexis-0.1.23.dist-info}/METADATA +2 -1
- {arthexis-0.1.22.dist-info → arthexis-0.1.23.dist-info}/RECORD +22 -22
- core/admin.py +85 -13
- core/models.py +484 -63
- core/release.py +0 -5
- core/tests.py +29 -1
- core/user_data.py +42 -2
- core/views.py +33 -26
- nodes/admin.py +64 -23
- nodes/models.py +9 -1
- nodes/tests.py +74 -0
- nodes/views.py +100 -48
- ocpp/admin.py +12 -1
- ocpp/models.py +29 -2
- ocpp/tests.py +123 -0
- ocpp/views.py +19 -3
- pages/tests.py +25 -6
- pages/urls.py +5 -0
- pages/views.py +84 -8
- {arthexis-0.1.22.dist-info → arthexis-0.1.23.dist-info}/WHEEL +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {arthexis-0.1.22.dist-info → arthexis-0.1.23.dist-info}/top_level.txt +0 -0
core/tests.py
CHANGED
|
@@ -2138,6 +2138,25 @@ class PackageReleaseCurrentTests(TestCase):
|
|
|
2138
2138
|
self.package.save()
|
|
2139
2139
|
self.assertFalse(self.release.is_current)
|
|
2140
2140
|
|
|
2141
|
+
def test_is_current_false_when_version_has_plus(self):
|
|
2142
|
+
self.version_path.write_text("1.0.0+")
|
|
2143
|
+
self.assertFalse(self.release.is_current)
|
|
2144
|
+
|
|
2145
|
+
|
|
2146
|
+
class PackageReleaseRevisionTests(TestCase):
|
|
2147
|
+
def setUp(self):
|
|
2148
|
+
self.package = Package.objects.get(name="arthexis")
|
|
2149
|
+
self.release = PackageRelease.objects.create(
|
|
2150
|
+
package=self.package,
|
|
2151
|
+
version="1.0.0",
|
|
2152
|
+
revision="abcdef123456",
|
|
2153
|
+
)
|
|
2154
|
+
|
|
2155
|
+
def test_matches_revision_ignores_plus_suffix(self):
|
|
2156
|
+
self.assertTrue(
|
|
2157
|
+
PackageRelease.matches_revision("1.0.0+", "abcdef123456")
|
|
2158
|
+
)
|
|
2159
|
+
|
|
2141
2160
|
def test_is_current_false_when_version_differs(self):
|
|
2142
2161
|
self.release.version = "2.0.0"
|
|
2143
2162
|
self.release.save()
|
|
@@ -2188,13 +2207,22 @@ class PackageAdminPrepareNextReleaseTests(TestCase):
|
|
|
2188
2207
|
|
|
2189
2208
|
def test_prepare_next_release_active_creates_release(self):
|
|
2190
2209
|
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2191
|
-
request = self.factory.
|
|
2210
|
+
request = self.factory.post("/admin/core/package/prepare-next-release/")
|
|
2192
2211
|
response = self.admin.prepare_next_release_active(request)
|
|
2193
2212
|
self.assertEqual(response.status_code, 302)
|
|
2194
2213
|
self.assertEqual(
|
|
2195
2214
|
PackageRelease.all_objects.filter(package=self.package).count(), 1
|
|
2196
2215
|
)
|
|
2197
2216
|
|
|
2217
|
+
def test_prepare_next_release_active_get_creates_release(self):
|
|
2218
|
+
PackageRelease.all_objects.filter(package=self.package).delete()
|
|
2219
|
+
request = self.factory.get("/admin/core/package/prepare-next-release/")
|
|
2220
|
+
response = self.admin.prepare_next_release_active(request)
|
|
2221
|
+
self.assertEqual(response.status_code, 302)
|
|
2222
|
+
self.assertTrue(
|
|
2223
|
+
PackageRelease.all_objects.filter(package=self.package).exists()
|
|
2224
|
+
)
|
|
2225
|
+
|
|
2198
2226
|
|
|
2199
2227
|
class PackageAdminChangeViewTests(TestCase):
|
|
2200
2228
|
def setUp(self):
|
core/user_data.py
CHANGED
|
@@ -201,9 +201,49 @@ def dump_user_fixture(instance, user=None) -> None:
|
|
|
201
201
|
|
|
202
202
|
def delete_user_fixture(instance, user=None) -> None:
|
|
203
203
|
target_user = user or _resolve_fixture_user(instance)
|
|
204
|
-
|
|
204
|
+
filename = (
|
|
205
|
+
f"{instance._meta.app_label}_{instance._meta.model_name}_{instance.pk}.json"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _remove_for_user(candidate) -> None:
|
|
209
|
+
if candidate is None:
|
|
210
|
+
return
|
|
211
|
+
base_path = Path(
|
|
212
|
+
getattr(candidate, "data_path", "") or Path(settings.BASE_DIR) / "data"
|
|
213
|
+
)
|
|
214
|
+
username = _username_for(candidate)
|
|
215
|
+
if not username:
|
|
216
|
+
return
|
|
217
|
+
user_dir = base_path / username
|
|
218
|
+
if user_dir.exists():
|
|
219
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
220
|
+
|
|
221
|
+
if target_user is not None:
|
|
222
|
+
_remove_for_user(target_user)
|
|
205
223
|
return
|
|
206
|
-
|
|
224
|
+
|
|
225
|
+
root = Path(settings.BASE_DIR) / "data"
|
|
226
|
+
if root.exists():
|
|
227
|
+
(root / filename).unlink(missing_ok=True)
|
|
228
|
+
for path in root.iterdir():
|
|
229
|
+
if path.is_dir():
|
|
230
|
+
(path / filename).unlink(missing_ok=True)
|
|
231
|
+
|
|
232
|
+
UserModel = get_user_model()
|
|
233
|
+
manager = getattr(UserModel, "all_objects", UserModel._default_manager)
|
|
234
|
+
for candidate in manager.all():
|
|
235
|
+
data_path = getattr(candidate, "data_path", "")
|
|
236
|
+
if not data_path:
|
|
237
|
+
continue
|
|
238
|
+
base_path = Path(data_path)
|
|
239
|
+
if not base_path.exists():
|
|
240
|
+
continue
|
|
241
|
+
username = _username_for(candidate)
|
|
242
|
+
if not username:
|
|
243
|
+
continue
|
|
244
|
+
user_dir = base_path / username
|
|
245
|
+
if user_dir.exists():
|
|
246
|
+
(user_dir / filename).unlink(missing_ok=True)
|
|
207
247
|
|
|
208
248
|
|
|
209
249
|
def _mark_fixture_user_data(path: Path) -> None:
|
core/views.py
CHANGED
|
@@ -690,16 +690,17 @@ def _ensure_origin_main_unchanged(log_path: Path) -> None:
|
|
|
690
690
|
def _next_patch_version(version: str) -> str:
|
|
691
691
|
from packaging.version import InvalidVersion, Version
|
|
692
692
|
|
|
693
|
+
cleaned = version.rstrip("+")
|
|
693
694
|
try:
|
|
694
|
-
parsed = Version(
|
|
695
|
+
parsed = Version(cleaned)
|
|
695
696
|
except InvalidVersion:
|
|
696
|
-
parts =
|
|
697
|
+
parts = cleaned.split(".") if cleaned else []
|
|
697
698
|
for index in range(len(parts) - 1, -1, -1):
|
|
698
699
|
segment = parts[index]
|
|
699
700
|
if segment.isdigit():
|
|
700
701
|
parts[index] = str(int(segment) + 1)
|
|
701
702
|
return ".".join(parts)
|
|
702
|
-
return version
|
|
703
|
+
return cleaned or version
|
|
703
704
|
return f"{parsed.major}.{parsed.minor}.{parsed.micro + 1}"
|
|
704
705
|
|
|
705
706
|
|
|
@@ -768,7 +769,9 @@ def _sync_release_with_revision(release: PackageRelease) -> tuple[bool, str]:
|
|
|
768
769
|
version_path = Path("VERSION")
|
|
769
770
|
if version_path.exists():
|
|
770
771
|
try:
|
|
771
|
-
|
|
772
|
+
raw_version = version_path.read_text(encoding="utf-8").strip()
|
|
773
|
+
cleaned_version = raw_version.rstrip("+") or "0.0.0"
|
|
774
|
+
repo_version = Version(cleaned_version)
|
|
772
775
|
except InvalidVersion:
|
|
773
776
|
repo_version = None
|
|
774
777
|
|
|
@@ -935,7 +938,7 @@ def _refresh_changelog_once(ctx, log_path: Path) -> None:
|
|
|
935
938
|
ctx["changelog_refreshed"] = True
|
|
936
939
|
|
|
937
940
|
|
|
938
|
-
def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
941
|
+
def _step_check_todos(release, ctx, log_path: Path, *, user=None) -> None:
|
|
939
942
|
_refresh_changelog_once(ctx, log_path)
|
|
940
943
|
|
|
941
944
|
pending_qs = Todo.objects.filter(is_deleted=False, done_on__isnull=True)
|
|
@@ -975,7 +978,7 @@ def _step_check_todos(release, ctx, log_path: Path) -> None:
|
|
|
975
978
|
ctx["todos_ack"] = True
|
|
976
979
|
|
|
977
980
|
|
|
978
|
-
def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
981
|
+
def _step_check_version(release, ctx, log_path: Path, *, user=None) -> None:
|
|
979
982
|
from . import release as release_utils
|
|
980
983
|
from packaging.version import InvalidVersion, Version
|
|
981
984
|
|
|
@@ -1110,10 +1113,12 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1110
1113
|
version_path = Path("VERSION")
|
|
1111
1114
|
if version_path.exists():
|
|
1112
1115
|
current = version_path.read_text(encoding="utf-8").strip()
|
|
1113
|
-
if current
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1116
|
+
if current:
|
|
1117
|
+
current_clean = current.rstrip("+") or "0.0.0"
|
|
1118
|
+
if Version(release.version) < Version(current_clean):
|
|
1119
|
+
raise Exception(
|
|
1120
|
+
f"Version {release.version} is older than existing {current}"
|
|
1121
|
+
)
|
|
1117
1122
|
|
|
1118
1123
|
_append_log(log_path, f"Checking if version {release.version} exists on PyPI")
|
|
1119
1124
|
if release_utils.network_available():
|
|
@@ -1163,17 +1168,17 @@ def _step_check_version(release, ctx, log_path: Path) -> None:
|
|
|
1163
1168
|
_append_log(log_path, "Network unavailable, skipping PyPI check")
|
|
1164
1169
|
|
|
1165
1170
|
|
|
1166
|
-
def _step_handle_migrations(release, ctx, log_path: Path) -> None:
|
|
1171
|
+
def _step_handle_migrations(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1167
1172
|
_append_log(log_path, "Freeze, squash and approve migrations")
|
|
1168
1173
|
_append_log(log_path, "Migration review acknowledged (manual step)")
|
|
1169
1174
|
|
|
1170
1175
|
|
|
1171
|
-
def _step_changelog_docs(release, ctx, log_path: Path) -> None:
|
|
1176
|
+
def _step_changelog_docs(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1172
1177
|
_append_log(log_path, "Compose CHANGELOG and documentation")
|
|
1173
1178
|
_append_log(log_path, "CHANGELOG and documentation review recorded")
|
|
1174
1179
|
|
|
1175
1180
|
|
|
1176
|
-
def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
1181
|
+
def _step_pre_release_actions(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1177
1182
|
_append_log(log_path, "Execute pre-release actions")
|
|
1178
1183
|
if ctx.get("dry_run"):
|
|
1179
1184
|
_append_log(log_path, "Dry run: skipping pre-release actions")
|
|
@@ -1249,12 +1254,12 @@ def _step_pre_release_actions(release, ctx, log_path: Path) -> None:
|
|
|
1249
1254
|
_append_log(log_path, "Pre-release actions complete")
|
|
1250
1255
|
|
|
1251
1256
|
|
|
1252
|
-
def _step_run_tests(release, ctx, log_path: Path) -> None:
|
|
1257
|
+
def _step_run_tests(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1253
1258
|
_append_log(log_path, "Complete test suite with --all flag")
|
|
1254
1259
|
_append_log(log_path, "Test suite completion acknowledged")
|
|
1255
1260
|
|
|
1256
1261
|
|
|
1257
|
-
def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
1262
|
+
def _step_promote_build(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1258
1263
|
from . import release as release_utils
|
|
1259
1264
|
|
|
1260
1265
|
_append_log(log_path, "Generating build files")
|
|
@@ -1266,7 +1271,7 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1266
1271
|
release_utils.promote(
|
|
1267
1272
|
package=release.to_package(),
|
|
1268
1273
|
version=release.version,
|
|
1269
|
-
creds=release.to_credentials(),
|
|
1274
|
+
creds=release.to_credentials(user=user),
|
|
1270
1275
|
)
|
|
1271
1276
|
_append_log(
|
|
1272
1277
|
log_path,
|
|
@@ -1314,8 +1319,10 @@ def _step_promote_build(release, ctx, log_path: Path) -> None:
|
|
|
1314
1319
|
_append_log(new_log, "Build complete")
|
|
1315
1320
|
|
|
1316
1321
|
|
|
1317
|
-
def _step_release_manager_approval(
|
|
1318
|
-
|
|
1322
|
+
def _step_release_manager_approval(
|
|
1323
|
+
release, ctx, log_path: Path, *, user=None
|
|
1324
|
+
) -> None:
|
|
1325
|
+
if release.to_credentials(user=user) is None:
|
|
1319
1326
|
ctx.pop("release_approval", None)
|
|
1320
1327
|
if not ctx.get("approval_credentials_missing"):
|
|
1321
1328
|
_append_log(log_path, "Release manager publishing credentials missing")
|
|
@@ -1349,14 +1356,14 @@ def _step_release_manager_approval(release, ctx, log_path: Path) -> None:
|
|
|
1349
1356
|
raise ApprovalRequired()
|
|
1350
1357
|
|
|
1351
1358
|
|
|
1352
|
-
def _step_publish(release, ctx, log_path: Path) -> None:
|
|
1359
|
+
def _step_publish(release, ctx, log_path: Path, *, user=None) -> None:
|
|
1353
1360
|
from . import release as release_utils
|
|
1354
1361
|
|
|
1355
1362
|
if ctx.get("dry_run"):
|
|
1356
1363
|
test_repository_url = os.environ.get(
|
|
1357
1364
|
"PYPI_TEST_REPOSITORY_URL", "https://test.pypi.org/legacy/"
|
|
1358
1365
|
)
|
|
1359
|
-
test_creds = release.to_credentials()
|
|
1366
|
+
test_creds = release.to_credentials(user=user)
|
|
1360
1367
|
if not (test_creds and test_creds.has_auth()):
|
|
1361
1368
|
test_creds = release_utils.Credentials(
|
|
1362
1369
|
token=os.environ.get("PYPI_TEST_API_TOKEN"),
|
|
@@ -1396,7 +1403,7 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1396
1403
|
release_utils.build(
|
|
1397
1404
|
package=package,
|
|
1398
1405
|
version=release.version,
|
|
1399
|
-
creds=release.to_credentials(),
|
|
1406
|
+
creds=release.to_credentials(user=user),
|
|
1400
1407
|
dist=True,
|
|
1401
1408
|
tests=False,
|
|
1402
1409
|
twine=False,
|
|
@@ -1425,13 +1432,13 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1425
1432
|
release_utils.publish(
|
|
1426
1433
|
package=release.to_package(),
|
|
1427
1434
|
version=release.version,
|
|
1428
|
-
creds=target.credentials or release.to_credentials(),
|
|
1435
|
+
creds=target.credentials or release.to_credentials(user=user),
|
|
1429
1436
|
repositories=[target],
|
|
1430
1437
|
)
|
|
1431
1438
|
_append_log(log_path, "Dry run: skipped release metadata updates")
|
|
1432
1439
|
return
|
|
1433
1440
|
|
|
1434
|
-
targets = release.build_publish_targets()
|
|
1441
|
+
targets = release.build_publish_targets(user=user)
|
|
1435
1442
|
repo_labels = []
|
|
1436
1443
|
for target in targets:
|
|
1437
1444
|
label = target.name
|
|
@@ -1450,7 +1457,7 @@ def _step_publish(release, ctx, log_path: Path) -> None:
|
|
|
1450
1457
|
release_utils.publish(
|
|
1451
1458
|
package=release.to_package(),
|
|
1452
1459
|
version=release.version,
|
|
1453
|
-
creds=release.to_credentials(),
|
|
1460
|
+
creds=release.to_credentials(user=user),
|
|
1454
1461
|
repositories=targets,
|
|
1455
1462
|
)
|
|
1456
1463
|
except release_utils.PostPublishWarning as warning:
|
|
@@ -1819,7 +1826,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1819
1826
|
return redirect(request.path)
|
|
1820
1827
|
|
|
1821
1828
|
manager = release.release_manager or release.package.release_manager
|
|
1822
|
-
credentials_ready = bool(release.to_credentials())
|
|
1829
|
+
credentials_ready = bool(release.to_credentials(user=request.user))
|
|
1823
1830
|
if credentials_ready and ctx.get("approval_credentials_missing"):
|
|
1824
1831
|
ctx.pop("approval_credentials_missing", None)
|
|
1825
1832
|
|
|
@@ -1990,7 +1997,7 @@ def release_progress(request, pk: int, action: str):
|
|
|
1990
1997
|
if to_run == step_count:
|
|
1991
1998
|
name, func = steps[to_run]
|
|
1992
1999
|
try:
|
|
1993
|
-
func(release, ctx, log_path)
|
|
2000
|
+
func(release, ctx, log_path, user=request.user)
|
|
1994
2001
|
except PendingTodos:
|
|
1995
2002
|
pass
|
|
1996
2003
|
except ApprovalRequired:
|
nodes/admin.py
CHANGED
|
@@ -19,7 +19,7 @@ from django.utils.html import format_html, format_html_join
|
|
|
19
19
|
from django.utils.translation import gettext_lazy as _
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
from types import SimpleNamespace
|
|
22
|
-
from urllib.parse import urlparse, urlsplit, urlunsplit
|
|
22
|
+
from urllib.parse import urljoin, urlparse, urlsplit, urlunsplit
|
|
23
23
|
import base64
|
|
24
24
|
import ipaddress
|
|
25
25
|
import json
|
|
@@ -440,6 +440,12 @@ class NodeAdmin(EntityModelAdmin):
|
|
|
440
440
|
},
|
|
441
441
|
"target": reverse("admin:index"),
|
|
442
442
|
}
|
|
443
|
+
mac_address = str(local_node.mac_address or "").strip()
|
|
444
|
+
if mac_address:
|
|
445
|
+
payload["requester_mac"] = mac_address
|
|
446
|
+
public_key = local_node.public_key
|
|
447
|
+
if public_key:
|
|
448
|
+
payload["requester_public_key"] = public_key
|
|
443
449
|
return payload
|
|
444
450
|
|
|
445
451
|
def _start_proxy_session(self, request, node):
|
|
@@ -474,29 +480,64 @@ class NodeAdmin(EntityModelAdmin):
|
|
|
474
480
|
}
|
|
475
481
|
|
|
476
482
|
last_error = ""
|
|
483
|
+
redirect_codes = {301, 302, 303, 307, 308}
|
|
484
|
+
|
|
477
485
|
for url in self._iter_remote_urls(node, "/nodes/proxy/session/"):
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
486
|
+
candidate_url = url
|
|
487
|
+
redirects_followed = 0
|
|
488
|
+
success = False
|
|
489
|
+
|
|
490
|
+
while True:
|
|
491
|
+
try:
|
|
492
|
+
response = requests.post(
|
|
493
|
+
candidate_url,
|
|
494
|
+
data=body,
|
|
495
|
+
headers=headers,
|
|
496
|
+
timeout=5,
|
|
497
|
+
allow_redirects=False,
|
|
498
|
+
)
|
|
499
|
+
except RequestException as exc:
|
|
500
|
+
last_error = str(exc)
|
|
501
|
+
break
|
|
502
|
+
|
|
503
|
+
if response.status_code in redirect_codes:
|
|
504
|
+
location = response.headers.get("Location")
|
|
505
|
+
if not location:
|
|
506
|
+
last_error = f"{response.status_code} redirect missing Location header"
|
|
507
|
+
break
|
|
508
|
+
|
|
509
|
+
redirects_followed += 1
|
|
510
|
+
if redirects_followed > 3:
|
|
511
|
+
last_error = "Too many redirects"
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
candidate_url = urljoin(candidate_url, location)
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
if not response.ok:
|
|
518
|
+
last_error = f"{response.status_code} {response.text}"
|
|
519
|
+
break
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
data = response.json()
|
|
523
|
+
except ValueError:
|
|
524
|
+
last_error = "Invalid JSON response"
|
|
525
|
+
break
|
|
526
|
+
|
|
527
|
+
login_url = data.get("login_url")
|
|
528
|
+
if not login_url:
|
|
529
|
+
last_error = "login_url missing"
|
|
530
|
+
break
|
|
531
|
+
|
|
532
|
+
success = True
|
|
533
|
+
break
|
|
534
|
+
|
|
535
|
+
if success:
|
|
536
|
+
return {
|
|
537
|
+
"ok": True,
|
|
538
|
+
"login_url": login_url,
|
|
539
|
+
"expires": data.get("expires"),
|
|
540
|
+
}
|
|
500
541
|
|
|
501
542
|
return {
|
|
502
543
|
"ok": False,
|
nodes/models.py
CHANGED
|
@@ -4,6 +4,7 @@ from collections.abc import Iterable
|
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from django.db import models
|
|
7
|
+
from django.db.models import Q
|
|
7
8
|
from django.db.utils import DatabaseError
|
|
8
9
|
from django.db.models.signals import post_delete
|
|
9
10
|
from django.dispatch import Signal, receiver
|
|
@@ -294,7 +295,14 @@ class Node(Entity):
|
|
|
294
295
|
"""Return the node representing the current host if it exists."""
|
|
295
296
|
mac = cls.get_current_mac()
|
|
296
297
|
try:
|
|
297
|
-
|
|
298
|
+
node = cls.objects.filter(mac_address__iexact=mac).first()
|
|
299
|
+
if node:
|
|
300
|
+
return node
|
|
301
|
+
return (
|
|
302
|
+
cls.objects.filter(current_relation=cls.Relation.SELF)
|
|
303
|
+
.filter(Q(mac_address__isnull=True) | Q(mac_address=""))
|
|
304
|
+
.first()
|
|
305
|
+
)
|
|
298
306
|
except DatabaseError:
|
|
299
307
|
logger.debug("nodes.Node.get_local skipped: database unavailable", exc_info=True)
|
|
300
308
|
return None
|
nodes/tests.py
CHANGED
|
@@ -1844,6 +1844,8 @@ class NodeAdminTests(TestCase):
|
|
|
1844
1844
|
mock_post.assert_called()
|
|
1845
1845
|
payload = json.loads(mock_post.call_args[1]["data"])
|
|
1846
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)
|
|
1847
1849
|
|
|
1848
1850
|
@patch("nodes.admin.requests.post")
|
|
1849
1851
|
def test_proxy_view_falls_back_to_http_after_ssl_error(self, mock_post):
|
|
@@ -1882,6 +1884,52 @@ class NodeAdminTests(TestCase):
|
|
|
1882
1884
|
self.assertIn("/nodes/proxy/session/", second_url)
|
|
1883
1885
|
payload = json.loads(mock_post.call_args_list[-1].kwargs["data"])
|
|
1884
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))
|
|
1885
1933
|
|
|
1886
1934
|
def test_proxy_link_displayed_for_remote_nodes(self):
|
|
1887
1935
|
Node.objects.create(
|
|
@@ -2629,6 +2677,32 @@ class NodeProxyGatewayTests(TestCase):
|
|
|
2629
2677
|
second = self.client.get(parsed.path)
|
|
2630
2678
|
self.assertEqual(second.status_code, 410)
|
|
2631
2679
|
|
|
2680
|
+
def test_proxy_session_accepts_mac_hint_when_uuid_unknown(self):
|
|
2681
|
+
payload = {
|
|
2682
|
+
"requester": str(uuid.uuid4()),
|
|
2683
|
+
"requester_mac": self.node.mac_address,
|
|
2684
|
+
"requester_public_key": self.node.public_key,
|
|
2685
|
+
"user": {
|
|
2686
|
+
"username": "proxy-user",
|
|
2687
|
+
"email": "proxy@example.com",
|
|
2688
|
+
"first_name": "Proxy",
|
|
2689
|
+
"last_name": "User",
|
|
2690
|
+
"is_staff": True,
|
|
2691
|
+
"is_superuser": True,
|
|
2692
|
+
"groups": [],
|
|
2693
|
+
"permissions": [],
|
|
2694
|
+
},
|
|
2695
|
+
"target": "/admin/",
|
|
2696
|
+
}
|
|
2697
|
+
body, signature = self._sign(payload)
|
|
2698
|
+
response = self.client.post(
|
|
2699
|
+
reverse("node-proxy-session"),
|
|
2700
|
+
data=body,
|
|
2701
|
+
content_type="application/json",
|
|
2702
|
+
HTTP_X_SIGNATURE=signature,
|
|
2703
|
+
)
|
|
2704
|
+
self.assertEqual(response.status_code, 200)
|
|
2705
|
+
|
|
2632
2706
|
def test_proxy_execute_lists_nodes(self):
|
|
2633
2707
|
Node.objects.create(
|
|
2634
2708
|
hostname="target",
|