feat(device-edge): add isolated B2 ingress domain
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
BUILDER = SCRIPT_DIR / "build-device-plane-backhaul-target-artifact.mjs"
|
||||
RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy"
|
||||
COMPOSE = (
|
||||
SCRIPT_DIR.parent.parent
|
||||
/ "device-plane/docker-compose.device-plane.backhaul-target.yml"
|
||||
)
|
||||
PREDECESSOR_COMPOSE = (
|
||||
SCRIPT_DIR.parent.parent / "device-plane/docker-compose.device-plane.yml"
|
||||
)
|
||||
SSHD_CONFIG = (
|
||||
SCRIPT_DIR.parent.parent
|
||||
/ "device-plane/services/device-backhaul-target/sshd_config"
|
||||
)
|
||||
|
||||
|
||||
def load_runner():
|
||||
loader = importlib.machinery.SourceFileLoader(
|
||||
"nodedc_device_plane_backhaul_runner_under_test",
|
||||
str(RUNNER_PATH),
|
||||
)
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
RUNNER = load_runner()
|
||||
|
||||
|
||||
def valid_public_key(comment="nodedc-device-edge-backhaul"):
|
||||
blob = b"\x00\x00\x00\x0bssh-ed25519\x00\x00\x00\x20" + bytes(range(32))
|
||||
return f"ssh-ed25519 {base64.b64encode(blob).decode()} {comment}\n"
|
||||
|
||||
|
||||
def healthy_inventory():
|
||||
return {
|
||||
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
|
||||
"composeProject": "nodedc-device-plane",
|
||||
"services": [
|
||||
{
|
||||
"service": service,
|
||||
"containerId": character * 64,
|
||||
"imageId": f"sha256:{character * 64}",
|
||||
"status": "running",
|
||||
"running": True,
|
||||
"health": "healthy",
|
||||
"restartCount": 0,
|
||||
}
|
||||
for service, character in (
|
||||
("device-control-core", "a"),
|
||||
("device-gateway", "b"),
|
||||
("device-postgres", "c"),
|
||||
)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class DevicePlaneBackhaulTargetArtifactTest(unittest.TestCase):
|
||||
def build(self, artifact_dir, patch_id):
|
||||
environment = os.environ.copy()
|
||||
environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir)
|
||||
result = subprocess.run(
|
||||
["node", str(BUILDER), patch_id],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=environment,
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
def test_artifact_is_exact_deterministic_and_contains_no_keys(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-plane-backhaul-artifact-",
|
||||
) as directory:
|
||||
target = Path(directory)
|
||||
first = self.build(target, "device-plane-backhaul-target-unit-001")
|
||||
first_bytes = Path(first["artifact"]).read_bytes()
|
||||
second = self.build(target, "device-plane-backhaul-target-unit-001")
|
||||
second_bytes = Path(second["artifact"]).read_bytes()
|
||||
self.assertEqual(first_bytes, second_bytes)
|
||||
self.assertEqual(first["sha256"], hashlib.sha256(first_bytes).hexdigest())
|
||||
self.assertEqual(
|
||||
first["entries"],
|
||||
list(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES),
|
||||
)
|
||||
self.assertEqual(first["services"], ["device-backhaul-target"])
|
||||
|
||||
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||
names = archive.getnames()
|
||||
files = archive.extractfile("files.txt").read().decode().splitlines()
|
||||
descriptor = json.loads(
|
||||
archive.extractfile(
|
||||
"payload/deployment/"
|
||||
"device-plane-backhaul-target-tailnet-serve-v1.json"
|
||||
).read()
|
||||
)
|
||||
self.assertEqual(files, first["entries"])
|
||||
self.assertEqual(
|
||||
descriptor,
|
||||
RUNNER.expected_device_plane_backhaul_target_descriptor(),
|
||||
)
|
||||
self.assertFalse(any(
|
||||
name.endswith((".key", ".pem", "authorized_keys"))
|
||||
for name in names
|
||||
))
|
||||
|
||||
def test_registry_selects_only_target_and_preserves_red_boundaries(self):
|
||||
entries = RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES
|
||||
self.assertEqual(
|
||||
RUNNER.component_services("device-plane", entries),
|
||||
("device-backhaul-target",),
|
||||
)
|
||||
builds = RUNNER.component_builds("device-plane", entries)
|
||||
self.assertEqual(len(builds), 1)
|
||||
self.assertIn("services/device-backhaul-target/Dockerfile", builds[0][1])
|
||||
self.assertIn(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_IMAGE, builds[0][1])
|
||||
for path in (
|
||||
RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL,
|
||||
"services/device-backhaul-target/Dockerfile",
|
||||
"services/device-backhaul-target/sshd_config",
|
||||
RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_REL,
|
||||
):
|
||||
self.assertTrue(RUNNER.allowed_payload_path("device-plane", path))
|
||||
|
||||
compose = COMPOSE.read_text(encoding="utf-8")
|
||||
sshd = SSHD_CONFIG.read_text(encoding="utf-8")
|
||||
self.assertIn("network_mode: host", compose)
|
||||
self.assertNotIn("0.0.0.0:2222", compose)
|
||||
self.assertIn('"127.0.0.1", "2222"', compose)
|
||||
self.assertIn("ListenAddress 127.0.0.1", sshd)
|
||||
self.assertIn("AllowTcpForwarding local", sshd)
|
||||
self.assertIn("PermitOpen 127.0.0.1:9921", sshd)
|
||||
self.assertIn("ForceCommand /bin/false", sshd)
|
||||
self.assertIn("PasswordAuthentication no", sshd)
|
||||
|
||||
def test_preflight_requires_exact_applied_006_and_enrollment_key(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-plane-backhaul-preflight-",
|
||||
) as directory:
|
||||
work = Path(directory)
|
||||
built = self.build(work, "device-plane-backhaul-target-unit-002")
|
||||
extracted = work / "extracted"
|
||||
extracted.mkdir()
|
||||
_manifest, _entries, payload = RUNNER.load_artifact(
|
||||
Path(built["artifact"]),
|
||||
extracted,
|
||||
)
|
||||
live = work / "live"
|
||||
live.mkdir()
|
||||
(live / "docker-compose.device-plane.yml").write_bytes(
|
||||
PREDECESSOR_COMPOSE.read_bytes()
|
||||
)
|
||||
descriptor = live / RUNNER.DEVICE_PLANE_B2_DISCOVERY_INGRESS_REL
|
||||
descriptor.parent.mkdir(parents=True)
|
||||
descriptor.write_text(
|
||||
json.dumps(RUNNER.expected_device_plane_b2_discovery_ingress_descriptor()),
|
||||
encoding="utf-8",
|
||||
)
|
||||
enrollment = work / "device-edge-backhaul.pub"
|
||||
enrollment.write_text(valid_public_key(), encoding="ascii")
|
||||
|
||||
def has_patch(value):
|
||||
return value == RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_PATCH_ID
|
||||
|
||||
def has_sha(value):
|
||||
return value == RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_PREDECESSOR_ARTIFACT_SHA256
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "component_root", return_value=live),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"DEVICE_PLANE_BACKHAUL_ENROLLMENT_PUBLIC_KEY_FILE",
|
||||
enrollment,
|
||||
),
|
||||
mock.patch.object(RUNNER, "state_has_patch_id", side_effect=has_patch),
|
||||
mock.patch.object(RUNNER, "state_has_sha", side_effect=has_sha),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"device_plane_service_container_ids",
|
||||
return_value=[],
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"device_plane_runtime_inventory",
|
||||
return_value=healthy_inventory(),
|
||||
),
|
||||
mock.patch.object(RUNNER, "assert_loopback_tcp_port_open"),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_backhaul_failed_evidence",
|
||||
return_value={
|
||||
"backup": work / "failed-backup",
|
||||
"failedArtifact": work / "failed-artifact.tgz",
|
||||
},
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_cli",
|
||||
return_value={
|
||||
"binary": str(RUNNER.DEVICE_PLANE_TAILSCALE),
|
||||
"uid": 1024,
|
||||
"gid": 1024,
|
||||
"binarySha256": "d" * 64,
|
||||
},
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_runtime",
|
||||
return_value={
|
||||
"self": {
|
||||
"Online": True,
|
||||
"TailscaleIPs": ["100.109.216.21"],
|
||||
},
|
||||
"serve": {},
|
||||
},
|
||||
),
|
||||
):
|
||||
accepted = RUNNER.validate_device_plane_backhaul_target_evidence(payload)
|
||||
self.assertEqual(
|
||||
accepted["mode"],
|
||||
"failed-backhaul-target-to-loopback-tailnet-serve",
|
||||
)
|
||||
self.assertRegex(accepted["enrollmentPublicKeySha256"], r"^[a-f0-9]{64}$")
|
||||
self.assertEqual(accepted["tailscaleServeBefore"], {})
|
||||
self.assertEqual(accepted["tailscaleCli"]["uid"], 1024)
|
||||
|
||||
def test_registered_health_gate_checks_preserved_and_target_services(self):
|
||||
with mock.patch.object(RUNNER, "healthcheck_compose_service") as health:
|
||||
RUNNER.run_healthchecks(
|
||||
"device-plane",
|
||||
RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES,
|
||||
(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
||||
)
|
||||
self.assertEqual(
|
||||
[call.args[1] for call in health.call_args_list],
|
||||
[
|
||||
"device-control-core",
|
||||
"device-gateway",
|
||||
"device-postgres",
|
||||
"device-backhaul-target",
|
||||
],
|
||||
)
|
||||
|
||||
def test_candidate_rollback_removes_only_target_and_preserves_runtime(self):
|
||||
runtime = healthy_inventory()
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"read_backup_path_list",
|
||||
side_effect=[[], list(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES)],
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_backup_partition",
|
||||
return_value=(set(), set(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES)),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"read_strict_json",
|
||||
side_effect=[runtime, {}],
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"disable_device_plane_tailscale_serve",
|
||||
) as disable_serve,
|
||||
mock.patch.object(RUNNER, "stop_and_remove_compose_services") as stop,
|
||||
mock.patch.object(RUNNER, "restore_platform_overlay", return_value=3),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"device_plane_service_container_ids",
|
||||
return_value=[],
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"device_plane_runtime_inventory",
|
||||
return_value=runtime,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_runtime",
|
||||
return_value={"serve": {}},
|
||||
),
|
||||
mock.patch.object(RUNNER, "assert_loopback_tcp_port_open") as port,
|
||||
):
|
||||
result = RUNNER.rollback_device_plane_apply(
|
||||
Path("/unused/live"),
|
||||
Path("/unused/backup"),
|
||||
RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES,
|
||||
"20260803-000000",
|
||||
True,
|
||||
(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
||||
)
|
||||
self.assertEqual(
|
||||
result,
|
||||
"tailscale-serve-restored-source-restored-target-removed-"
|
||||
"preserved-runtime-unchanged:3",
|
||||
)
|
||||
stop.assert_called_once_with(
|
||||
"device-plane",
|
||||
(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
||||
)
|
||||
disable_serve.assert_called_once_with({})
|
||||
port.assert_called_once_with(9921)
|
||||
|
||||
def test_tailscale_serve_port_parser_rejects_funnel_and_nested_collision(self):
|
||||
clean = {"TCP": {"443": {"HTTPS": True}}}
|
||||
self.assertEqual(
|
||||
RUNNER.device_plane_tailscale_handlers_for_port(clean, 2222),
|
||||
[],
|
||||
)
|
||||
active = {
|
||||
"TCP": {"2222": {"TCPForward": "127.0.0.1:2222"}},
|
||||
"Foreground": {
|
||||
"session": {
|
||||
"TCP": {"443": {"HTTPS": True}},
|
||||
},
|
||||
},
|
||||
}
|
||||
self.assertEqual(
|
||||
RUNNER.device_plane_tailscale_handlers_for_port(active, 2222),
|
||||
[((), {"TCPForward": "127.0.0.1:2222"})],
|
||||
)
|
||||
self.assertFalse(
|
||||
RUNNER.device_plane_tailscale_funnel_uses_port(active, 2222)
|
||||
)
|
||||
active["AllowFunnel"] = {"edge.example.ts.net:2222": True}
|
||||
self.assertTrue(
|
||||
RUNNER.device_plane_tailscale_funnel_uses_port(active, 2222)
|
||||
)
|
||||
|
||||
def test_runtime_activation_enables_private_tailscale_serve_after_health(self):
|
||||
calls = []
|
||||
with (
|
||||
mock.patch.object(RUNNER, "run_build", side_effect=lambda *a: calls.append("build")),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"prepare_component_runtime",
|
||||
side_effect=lambda *a: calls.append("prepare"),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"run_compose",
|
||||
side_effect=lambda *a: calls.append("compose"),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"healthcheck_compose_service",
|
||||
side_effect=lambda *a: calls.append("health"),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"enable_device_plane_tailscale_serve",
|
||||
side_effect=lambda *a: calls.append("serve"),
|
||||
) as enable,
|
||||
):
|
||||
RUNNER.run_device_plane_runtime_for_apply(
|
||||
RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_ENTRIES,
|
||||
(RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,),
|
||||
lambda: calls.append("started"),
|
||||
backhaul_serve_before={"TCP": {"443": {"HTTPS": True}}},
|
||||
)
|
||||
self.assertEqual(
|
||||
calls,
|
||||
["build", "prepare", "started", "compose", "health", "serve"],
|
||||
)
|
||||
enable.assert_called_once_with({"TCP": {"443": {"HTTPS": True}}})
|
||||
|
||||
def test_tailscale_serve_enable_and_disable_preserve_unrelated_routes(self):
|
||||
before = {"TCP": {"443": {"HTTPS": True}}}
|
||||
active = {
|
||||
"TCP": {
|
||||
"443": {"HTTPS": True},
|
||||
"2222": {"TCPForward": "127.0.0.1:2222"},
|
||||
},
|
||||
}
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_runtime",
|
||||
side_effect=[{"serve": before}, {"serve": active}],
|
||||
),
|
||||
mock.patch.object(RUNNER, "run_device_plane_tailscale") as run,
|
||||
):
|
||||
result = RUNNER.enable_device_plane_tailscale_serve(before)
|
||||
self.assertEqual(result, active)
|
||||
self.assertEqual(
|
||||
run.call_args.args[0],
|
||||
[
|
||||
"serve",
|
||||
"--bg",
|
||||
"--yes",
|
||||
"--tcp=2222",
|
||||
"tcp://127.0.0.1:2222",
|
||||
],
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"read_device_plane_tailscale_json",
|
||||
return_value=active,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_runtime",
|
||||
return_value={"serve": before},
|
||||
),
|
||||
mock.patch.object(RUNNER, "run_device_plane_tailscale") as run,
|
||||
):
|
||||
changed = RUNNER.disable_device_plane_tailscale_serve(before)
|
||||
self.assertTrue(changed)
|
||||
self.assertEqual(
|
||||
run.call_args.args[0],
|
||||
[
|
||||
"serve",
|
||||
"--tcp=2222",
|
||||
"off",
|
||||
],
|
||||
)
|
||||
|
||||
def test_tailscale_cli_runs_as_official_package_account(self):
|
||||
context = {
|
||||
"binary": "/var/packages/Tailscale/target/bin/tailscale",
|
||||
"uid": 1051,
|
||||
"gid": 1051,
|
||||
"binarySha256": "e" * 64,
|
||||
}
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_tailscale_cli",
|
||||
return_value=context,
|
||||
),
|
||||
mock.patch.object(RUNNER.subprocess, "run") as run,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"device_plane_tailscale_drop_privileges",
|
||||
return_value="drop-to-package-account",
|
||||
) as drop,
|
||||
):
|
||||
RUNNER.run_device_plane_tailscale(
|
||||
["status", "--json"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args.args[0],
|
||||
[context["binary"], "status", "--json"],
|
||||
)
|
||||
self.assertEqual(
|
||||
run.call_args.kwargs["preexec_fn"],
|
||||
"drop-to-package-account",
|
||||
)
|
||||
drop.assert_called_once_with(1051, 1051)
|
||||
|
||||
def test_tailscale_cli_accepts_package_owned_binary_without_root_execution(self):
|
||||
privilege_path = mock.MagicMock()
|
||||
privilege_path.__str__.return_value = (
|
||||
"/var/packages/Tailscale/conf/privilege"
|
||||
)
|
||||
privilege_path.lstat.return_value = SimpleNamespace(
|
||||
st_mode=RUNNER.stat.S_IFREG | 0o644,
|
||||
st_uid=0,
|
||||
)
|
||||
binary_path = mock.MagicMock()
|
||||
binary_path.__str__.return_value = (
|
||||
"/var/packages/Tailscale/target/bin/tailscale"
|
||||
)
|
||||
binary_path.lstat.return_value = SimpleNamespace(
|
||||
st_mode=RUNNER.stat.S_IFREG | 0o755,
|
||||
st_uid=1051,
|
||||
st_gid=1051,
|
||||
st_size=32 * 1024 * 1024,
|
||||
)
|
||||
account = SimpleNamespace(pw_uid=1051, pw_gid=1051)
|
||||
group = SimpleNamespace(gr_gid=1051)
|
||||
help_result = SimpleNamespace(
|
||||
stdout="--tcp --bg --yes",
|
||||
stderr="",
|
||||
)
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"DEVICE_PLANE_TAILSCALE_PRIVILEGE",
|
||||
privilege_path,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"DEVICE_PLANE_TAILSCALE",
|
||||
binary_path,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"read_strict_json",
|
||||
return_value={
|
||||
"defaults": {"run-as": "package"},
|
||||
"username": "tailscale",
|
||||
"groupname": "tailscale",
|
||||
},
|
||||
),
|
||||
mock.patch.object(RUNNER.pwd, "getpwnam", return_value=account),
|
||||
mock.patch.object(RUNNER.grp, "getgrnam", return_value=group),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"sha256_file",
|
||||
return_value="f" * 64,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER.subprocess,
|
||||
"run",
|
||||
return_value=help_result,
|
||||
) as run,
|
||||
):
|
||||
context = RUNNER.validate_device_plane_tailscale_cli()
|
||||
self.assertEqual(context["uid"], 1051)
|
||||
self.assertEqual(context["gid"], 1051)
|
||||
self.assertEqual(context["binarySha256"], "f" * 64)
|
||||
self.assertTrue(callable(run.call_args.kwargs["preexec_fn"]))
|
||||
|
||||
def test_failed_001_evidence_is_exact_and_terminal(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-plane-backhaul-failed-evidence-",
|
||||
) as directory:
|
||||
root = Path(directory)
|
||||
backups = root / "backups"
|
||||
failed = root / "failed"
|
||||
state = root / "state"
|
||||
tmp = root / "tmp"
|
||||
for path in (backups, failed, state, tmp):
|
||||
path.mkdir()
|
||||
|
||||
backup = backups / RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID
|
||||
backup.mkdir()
|
||||
backup_hashes = {}
|
||||
for name in (
|
||||
"manifest.env",
|
||||
"files.txt",
|
||||
"source-before.tgz",
|
||||
"existing-files.txt",
|
||||
"missing-files.txt",
|
||||
"runtime-before.json",
|
||||
):
|
||||
payload = f"fixture:{name}\n".encode()
|
||||
(backup / name).write_bytes(payload)
|
||||
backup_hashes[name] = hashlib.sha256(payload).hexdigest()
|
||||
|
||||
stage = root / "failed-stage"
|
||||
payload = stage / "payload"
|
||||
service = payload / "services/device-backhaul-target"
|
||||
deployment = payload / "deployment"
|
||||
service.mkdir(parents=True)
|
||||
deployment.mkdir(parents=True)
|
||||
(stage / "manifest.env").write_text(
|
||||
"id=device-plane-backhaul-target-20260803-001\n"
|
||||
"component=device-plane\n"
|
||||
"type=app-overlay\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(stage / "files.txt").write_text(
|
||||
"\n".join(RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_TARGET_ENTRIES)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(payload / RUNNER.DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL).write_text(
|
||||
"services: {}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(service / "Dockerfile").write_text(
|
||||
"FROM scratch\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(service / "sshd_config").write_text(
|
||||
"PasswordAuthentication no\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(payload / RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_TARGET_REL).write_text(
|
||||
json.dumps(
|
||||
RUNNER.expected_failed_device_plane_backhaul_target_descriptor()
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
failed_artifact = failed / RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT
|
||||
with tarfile.open(failed_artifact, "w:gz") as archive:
|
||||
for name in ("manifest.env", "files.txt", "payload"):
|
||||
archive.add(stage / name, arcname=name)
|
||||
failed_sha = hashlib.sha256(failed_artifact.read_bytes()).hexdigest()
|
||||
|
||||
failed_state = state / "failed.jsonl"
|
||||
failed_state.write_text(
|
||||
json.dumps({
|
||||
"artifact": RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT,
|
||||
"backup_id": RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_ID,
|
||||
"component": "device-plane",
|
||||
"id": RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_PATCH_ID,
|
||||
"message": RUNNER.DEVICE_PLANE_BACKHAUL_FAILED_MESSAGE,
|
||||
"rollback_status": (
|
||||
"ok:device-plane-overlay:source-restored-target-removed-"
|
||||
"preserved-runtime-unchanged:3"
|
||||
),
|
||||
"sha256": failed_sha,
|
||||
"started_apply": True,
|
||||
"status": "failed",
|
||||
})
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(RUNNER, "BACKUPS_DIR", backups),
|
||||
mock.patch.object(RUNNER, "FAILED_DIR", failed),
|
||||
mock.patch.object(RUNNER, "FAILED_STATE_FILE", failed_state),
|
||||
mock.patch.object(RUNNER, "TMP_DIR", tmp),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"DEVICE_PLANE_BACKHAUL_FAILED_BACKUP_SHA256",
|
||||
backup_hashes,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"DEVICE_PLANE_BACKHAUL_FAILED_ARTIFACT_SHA256",
|
||||
failed_sha,
|
||||
),
|
||||
):
|
||||
evidence = RUNNER.validate_device_plane_backhaul_failed_evidence()
|
||||
self.assertEqual(evidence["backup"], backup)
|
||||
self.assertEqual(evidence["failedArtifact"], failed_artifact)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user