merge: canonicalize Device Plane release runner
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import importlib.machinery
|
import importlib.machinery
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -27,6 +29,617 @@ RUNNER = load_runner()
|
|||||||
|
|
||||||
|
|
||||||
class DevicePlaneRegistryTest(unittest.TestCase):
|
class DevicePlaneRegistryTest(unittest.TestCase):
|
||||||
|
def test_manager_v7_pins_v6_and_live_edge_host_projection(self):
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-manager-release-v6-20260822-035",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"193faabe930e2b3f212f8eb45288e39f850ec714528b28881095be654baf9a80",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-control-core-release-v2-20260822-036",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"8708cc4b59fa0cd5e9c6e6a7b2654ba01ea60271549167aca2631f94000d3da3",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_COMPOSE_SHA256,
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
|
||||||
|
)
|
||||||
|
boundaries = RUNNER.expected_device_plane_manager_release_v7_boundaries()
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["infrastructureHostProjection"],
|
||||||
|
"edge-registration-live-channel-v1",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["ontologyStatus"],
|
||||||
|
"generic-host-domain-candidate-not-canonical",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V7_ENTRIES,
|
||||||
|
),
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v6_pins_034_and_canonical_favicon_boundary(self):
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-manager-release-v5-20260822-034",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
|
||||||
|
)
|
||||||
|
boundaries = RUNNER.expected_device_plane_manager_release_v6_boundaries()
|
||||||
|
self.assertEqual(boundaries["faviconSet"], "nodedc-adaptive-v1")
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["overviewLayout"],
|
||||||
|
"mission-core-landing-stage-v1",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
|
||||||
|
),
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v5_pins_033_and_mission_core_overview_layout(self):
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-manager-release-v4-20260822-033",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
|
||||||
|
)
|
||||||
|
boundaries = RUNNER.expected_device_plane_manager_release_v5_boundaries()
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["overviewLayout"],
|
||||||
|
"mission-core-landing-stage-v1",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.component_services(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
|
||||||
|
),
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v4_pins_032_and_persistent_white_boundary(self):
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-manager-release-v3-20260822-032",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"6e0eb3a0a6f19ceab92d46832b93bffbcea21247dbdc2ea50625a51ff460e4ca",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
|
||||||
|
"e7dff0f5873ad4586bd55946d3db2bb86092a5e149e886d120adc041e056c256",
|
||||||
|
)
|
||||||
|
boundaries = RUNNER.expected_device_plane_manager_release_v4_boundaries()
|
||||||
|
self.assertEqual(boundaries["defaultAccentHex"], "#f5f5f5")
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["presentationDataHostPath"],
|
||||||
|
"/volume1/docker/nodedc-device-plane/data/device-manager",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
boundaries["presentationDataContainerPath"],
|
||||||
|
"/var/lib/nodedc-device-manager",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v3_targets_applied_control_core_recovery(self):
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID,
|
||||||
|
"device-control-core-release-v2-20260821-030",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256,
|
||||||
|
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
RUNNER.expected_device_plane_manager_release_v3_boundaries()[
|
||||||
|
"controlCorePredecessor"
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"patchId": (
|
||||||
|
"device-control-core-release-v2-20260821-030"
|
||||||
|
),
|
||||||
|
"artifactSha256": (
|
||||||
|
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_control_core_predecessor_health_is_phase_scoped(self):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RUNNER.DeployError,
|
||||||
|
"preflight phase is invalid",
|
||||||
|
):
|
||||||
|
RUNNER.validate_device_plane_control_core_release_predecessor(
|
||||||
|
Path("/not-used"),
|
||||||
|
preflight_phase="unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
plan_source = inspect.getsource(RUNNER.plan_artifact)
|
||||||
|
self.assertIn(
|
||||||
|
'validate_device_plane_control_core_release_predecessor(\n'
|
||||||
|
' payload_dir,\n'
|
||||||
|
' preflight_phase="plan",',
|
||||||
|
plan_source,
|
||||||
|
)
|
||||||
|
apply_source = inspect.getsource(RUNNER.apply_artifact)
|
||||||
|
self.assertIn(
|
||||||
|
'validate_device_plane_control_core_release_predecessor(\n'
|
||||||
|
' payload_dir,\n'
|
||||||
|
' preflight_phase="apply",',
|
||||||
|
apply_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_control_core_preflight_health_excludes_selected_target(self):
|
||||||
|
descriptor = {
|
||||||
|
"preservedServices": [
|
||||||
|
"device-manager",
|
||||||
|
"device-gateway",
|
||||||
|
"device-postgres",
|
||||||
|
"device-backhaul-target",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service",
|
||||||
|
) as healthcheck:
|
||||||
|
services = (
|
||||||
|
RUNNER.validate_device_plane_control_core_preserved_runtime_health(
|
||||||
|
descriptor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
services,
|
||||||
|
(
|
||||||
|
"device-manager",
|
||||||
|
"device-gateway",
|
||||||
|
"device-postgres",
|
||||||
|
"device-backhaul-target",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
healthcheck.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call("device-plane", "device-manager"),
|
||||||
|
mock.call("device-plane", "device-gateway"),
|
||||||
|
mock.call("device-plane", "device-postgres"),
|
||||||
|
mock.call("device-plane", "device-backhaul-target"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
mock.call("device-plane", "device-control-core"),
|
||||||
|
healthcheck.call_args_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_control_core_selected_predecessor_may_be_unhealthy(self):
|
||||||
|
inventory = {
|
||||||
|
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
|
||||||
|
"composeProject": "nodedc-device-plane",
|
||||||
|
"services": [{
|
||||||
|
"service": "device-control-core",
|
||||||
|
"containerId": "a" * 64,
|
||||||
|
"imageId": "sha256:" + "b" * 64,
|
||||||
|
"status": "running",
|
||||||
|
"running": True,
|
||||||
|
"health": "unhealthy",
|
||||||
|
"restartCount": 4,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"device_plane_runtime_inventory",
|
||||||
|
return_value=inventory,
|
||||||
|
):
|
||||||
|
selected = (
|
||||||
|
RUNNER.validate_device_plane_control_core_selected_predecessor_runtime()
|
||||||
|
)
|
||||||
|
self.assertEqual(selected["health"], "unhealthy")
|
||||||
|
|
||||||
|
def test_control_core_selected_predecessor_may_be_restarting(self):
|
||||||
|
inventory = {
|
||||||
|
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
|
||||||
|
"composeProject": "nodedc-device-plane",
|
||||||
|
"services": [{
|
||||||
|
"service": "device-control-core",
|
||||||
|
"containerId": "a" * 64,
|
||||||
|
"imageId": "sha256:" + "b" * 64,
|
||||||
|
"status": "restarting",
|
||||||
|
"running": True,
|
||||||
|
"health": "starting",
|
||||||
|
"restartCount": 5,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"device_plane_runtime_inventory",
|
||||||
|
return_value=inventory,
|
||||||
|
):
|
||||||
|
selected = (
|
||||||
|
RUNNER.validate_device_plane_control_core_selected_predecessor_runtime()
|
||||||
|
)
|
||||||
|
self.assertEqual(selected["status"], "restarting")
|
||||||
|
self.assertEqual(selected["health"], "starting")
|
||||||
|
|
||||||
|
def test_control_core_rollback_accepts_restored_unhealthy_boundary(self):
|
||||||
|
service_names = (
|
||||||
|
"device-control-core",
|
||||||
|
"device-manager",
|
||||||
|
"device-gateway",
|
||||||
|
"device-postgres",
|
||||||
|
"device-backhaul-target",
|
||||||
|
)
|
||||||
|
inventory = {
|
||||||
|
"schemaVersion": "nodedc.device-plane.runtime-inventory.v1",
|
||||||
|
"composeProject": "nodedc-device-plane",
|
||||||
|
"services": [
|
||||||
|
{
|
||||||
|
"service": service,
|
||||||
|
"containerId": chr(97 + index) * 64,
|
||||||
|
"imageId": "sha256:" + str(index + 1) * 64,
|
||||||
|
"status": "running",
|
||||||
|
"running": True,
|
||||||
|
"health": "unhealthy" if index == 0 else "healthy",
|
||||||
|
"restartCount": index,
|
||||||
|
}
|
||||||
|
for index, service in enumerate(service_names)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
restored_id = "f" * 64
|
||||||
|
restored = {
|
||||||
|
"Id": restored_id,
|
||||||
|
"State": {
|
||||||
|
"Status": "running",
|
||||||
|
"Running": True,
|
||||||
|
"Health": {"Status": "unhealthy"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
) as preserved_health,
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"compose_service_container_id",
|
||||||
|
return_value=restored_id,
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"inspect_device_plane_container",
|
||||||
|
return_value=restored,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
accepted = (
|
||||||
|
RUNNER.accept_device_plane_control_core_rollback_runtime(
|
||||||
|
inventory
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(accepted["health"], "unhealthy")
|
||||||
|
self.assertEqual(accepted["status"], "running")
|
||||||
|
self.assertEqual(accepted["predecessorHealth"], "unhealthy")
|
||||||
|
self.assertEqual(
|
||||||
|
preserved_health.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call("device-plane", "device-manager"),
|
||||||
|
mock.call("device-plane", "device-gateway"),
|
||||||
|
mock.call("device-plane", "device-postgres"),
|
||||||
|
mock.call("device-plane", "device-backhaul-target"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_control_core_post_apply_health_includes_backhaul(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
) as healthcheck,
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_healthchecks",
|
||||||
|
return_value=(),
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_manager_control_plane_runtime",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
RUNNER.run_healthchecks(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES,
|
||||||
|
("device-control-core",),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
healthcheck.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call("device-plane", "device-control-core"),
|
||||||
|
mock.call("device-plane", "device-manager"),
|
||||||
|
mock.call("device-plane", "device-gateway"),
|
||||||
|
mock.call("device-plane", "device-postgres"),
|
||||||
|
mock.call("device-plane", "device-backhaul-target"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v3_post_apply_uses_private_egress_core_boundary(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_healthchecks",
|
||||||
|
return_value=(),
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_manager_control_plane_runtime",
|
||||||
|
) as runtime_acceptance,
|
||||||
|
):
|
||||||
|
RUNNER.run_healthchecks(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_acceptance.assert_called_once_with(
|
||||||
|
require_edge_channel=True,
|
||||||
|
core_network_mode="private-egress",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v4_post_apply_requires_persistent_data(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_healthchecks",
|
||||||
|
return_value=(),
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_manager_control_plane_runtime",
|
||||||
|
) as runtime_acceptance,
|
||||||
|
):
|
||||||
|
RUNNER.run_healthchecks(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_acceptance.assert_called_once_with(
|
||||||
|
require_edge_channel=True,
|
||||||
|
core_network_mode="private-egress",
|
||||||
|
require_persistent_data=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v5_post_apply_preserves_persistent_data_gate(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_healthchecks",
|
||||||
|
return_value=(),
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_manager_control_plane_runtime",
|
||||||
|
) as runtime_acceptance,
|
||||||
|
):
|
||||||
|
RUNNER.run_healthchecks(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_acceptance.assert_called_once_with(
|
||||||
|
require_edge_channel=True,
|
||||||
|
core_network_mode="private-egress",
|
||||||
|
require_persistent_data=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v6_post_apply_preserves_persistent_data_gate(self):
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service_with_grace",
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"component_healthchecks",
|
||||||
|
return_value=(),
|
||||||
|
),
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_manager_control_plane_runtime",
|
||||||
|
) as runtime_acceptance,
|
||||||
|
):
|
||||||
|
RUNNER.run_healthchecks(
|
||||||
|
"device-plane",
|
||||||
|
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
|
||||||
|
("device-manager",),
|
||||||
|
)
|
||||||
|
|
||||||
|
runtime_acceptance.assert_called_once_with(
|
||||||
|
require_edge_channel=True,
|
||||||
|
core_network_mode="private-egress",
|
||||||
|
require_persistent_data=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_manager_v4_prepare_owns_new_parent_and_managed_data_directory(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-manager-v4-data-",
|
||||||
|
) as directory:
|
||||||
|
data_dir = Path(directory) / "data" / "device-manager"
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"DEVICE_PLANE_MANAGER_DATA_DIR",
|
||||||
|
data_dir,
|
||||||
|
),
|
||||||
|
mock.patch.object(RUNNER.os, "chown") as chown,
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_plane_manager_persistent_data_metadata",
|
||||||
|
return_value="uid-1000-gid-1000-mode-0750",
|
||||||
|
) as validate,
|
||||||
|
):
|
||||||
|
result = RUNNER.ensure_device_plane_manager_persistent_data()
|
||||||
|
|
||||||
|
self.assertEqual(result, "uid-1000-gid-1000-mode-0750")
|
||||||
|
self.assertEqual(
|
||||||
|
chown.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call(data_dir.parent, 0, 0),
|
||||||
|
mock.call(data_dir, 1000, 1000),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
validate.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_manager_v4_prepare_preserves_existing_safe_parent_metadata(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-manager-v4-parent-",
|
||||||
|
) as directory:
|
||||||
|
data_parent = Path(directory) / "data"
|
||||||
|
data_parent.mkdir(mode=0o700)
|
||||||
|
data_dir = data_parent / "device-manager"
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"DEVICE_PLANE_MANAGER_DATA_DIR",
|
||||||
|
data_dir,
|
||||||
|
),
|
||||||
|
mock.patch.object(RUNNER.os, "chown") as chown,
|
||||||
|
mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"validate_device_plane_manager_persistent_data_metadata",
|
||||||
|
return_value="uid-1000-gid-1000-mode-0750",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
RUNNER.ensure_device_plane_manager_persistent_data()
|
||||||
|
|
||||||
|
self.assertEqual(stat.S_IMODE(data_parent.stat().st_mode), 0o700)
|
||||||
|
chown.assert_called_once_with(data_dir, 1000, 1000)
|
||||||
|
|
||||||
|
def test_manager_v4_prepare_rejects_symlink_parent_before_child_creation(self):
|
||||||
|
with tempfile.TemporaryDirectory(
|
||||||
|
prefix="nodedc-manager-v4-symlink-",
|
||||||
|
) as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
outside = root / "outside"
|
||||||
|
outside.mkdir()
|
||||||
|
data_parent = root / "data"
|
||||||
|
data_parent.symlink_to(outside, target_is_directory=True)
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"DEVICE_PLANE_MANAGER_DATA_DIR",
|
||||||
|
data_parent / "device-manager",
|
||||||
|
):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RUNNER.DeployError,
|
||||||
|
"persistent data parent is unsafe",
|
||||||
|
):
|
||||||
|
RUNNER.ensure_device_plane_manager_persistent_data()
|
||||||
|
self.assertFalse((outside / "device-manager").exists())
|
||||||
|
|
||||||
|
def test_manager_predecessor_health_is_phase_scoped(self):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
RUNNER.DeployError,
|
||||||
|
"preflight phase is invalid",
|
||||||
|
):
|
||||||
|
RUNNER.validate_device_plane_manager_activation_predecessor(
|
||||||
|
Path("/not-used"),
|
||||||
|
preflight_phase="unknown",
|
||||||
|
)
|
||||||
|
|
||||||
|
plan_source = inspect.getsource(RUNNER.plan_artifact)
|
||||||
|
self.assertIn('preflight_phase="plan"', plan_source)
|
||||||
|
apply_source = inspect.getsource(RUNNER.apply_artifact)
|
||||||
|
self.assertIn('preflight_phase="apply"', apply_source)
|
||||||
|
|
||||||
|
def test_manager_v3_health_gate_checks_only_preserved_services(self):
|
||||||
|
descriptor = {
|
||||||
|
"schemaVersion": (
|
||||||
|
"nodedc.device-plane.device-manager-release.v3"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service",
|
||||||
|
) as healthcheck:
|
||||||
|
services = (
|
||||||
|
RUNNER.validate_device_plane_manager_preserved_runtime_health(
|
||||||
|
descriptor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
services,
|
||||||
|
("device-control-core", "device-postgres"),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
healthcheck.call_args_list,
|
||||||
|
[
|
||||||
|
mock.call("device-plane", "device-control-core"),
|
||||||
|
mock.call("device-plane", "device-postgres"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
mock.call("device-plane", "device-manager"),
|
||||||
|
healthcheck.call_args_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_legacy_manager_health_gate_excludes_selected_services(self):
|
||||||
|
descriptor = {
|
||||||
|
"schemaVersion": (
|
||||||
|
"nodedc.device-plane.device-manager-release.v2"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
with mock.patch.object(
|
||||||
|
RUNNER,
|
||||||
|
"healthcheck_compose_service",
|
||||||
|
) as healthcheck:
|
||||||
|
services = (
|
||||||
|
RUNNER.validate_device_plane_manager_preserved_runtime_health(
|
||||||
|
descriptor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(services, ("device-postgres",))
|
||||||
|
healthcheck.assert_called_once_with(
|
||||||
|
"device-plane",
|
||||||
|
"device-postgres",
|
||||||
|
)
|
||||||
|
|
||||||
def test_registry_has_exact_roots_project_and_stateless_services(self):
|
def test_registry_has_exact_roots_project_and_stateless_services(self):
|
||||||
component = RUNNER.COMPONENTS["device-plane"]
|
component = RUNNER.COMPONENTS["device-plane"]
|
||||||
root = Path("/volume1/docker/nodedc-device-plane")
|
root = Path("/volume1/docker/nodedc-device-plane")
|
||||||
|
|||||||
Reference in New Issue
Block a user