deploy: register Manager v4 persistent data

This commit is contained in:
Codex
2026-08-22 09:47:57 +03:00
parent 12901e0e19
commit 6123a06527
2 changed files with 381 additions and 17 deletions
+252 -17
View File
@@ -66,6 +66,14 @@ PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env")
DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime")
DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane")
DEVICE_PLANE_SECRET_DIR = DEVICE_PLANE_ROOT / "secrets"
DEVICE_PLANE_MANAGER_DATA_DIR = DEVICE_PLANE_ROOT / "data" / "device-manager"
DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR = "/var/lib/nodedc-device-manager"
DEVICE_PLANE_MANAGER_PRESENTATION_PATH = (
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}/device-manager-presentation.json"
)
DEVICE_PLANE_MANAGER_MEDIA_ROOT = (
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}/media"
)
DEVICE_PLANE_POSTGRES_PASSWORD_FILE = DEVICE_PLANE_SECRET_DIR / "postgres-password"
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE = DEVICE_PLANE_SECRET_DIR / "gateway-core-token"
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepper"
@@ -249,6 +257,9 @@ DEVICE_PLANE_MANAGER_RELEASE_V2_REL = (
DEVICE_PLANE_MANAGER_RELEASE_V3_REL = (
"deployment/device-manager-release-v3.json"
)
DEVICE_PLANE_MANAGER_RELEASE_V4_REL = (
"deployment/device-manager-release-v4.json"
)
DEVICE_PLANE_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = (
"deployment/device-edge-core-channel-bootstrap-v1.json"
@@ -356,6 +367,12 @@ DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = (
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256 = (
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
)
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID = (
"device-manager-release-v3-20260822-032"
)
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256 = (
"6e0eb3a0a6f19ceab92d46832b93bffbcea21247dbdc2ea50625a51ff460e4ca"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-bootstrap-20260812-018"
)
@@ -414,6 +431,9 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_INVALID_CERTIFICATE_FINGERPRINT = (
DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 = (
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e"
)
DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256 = (
"e7dff0f5873ad4586bd55946d3db2bb86092a5e149e886d120adc041e056c256"
)
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
)
@@ -492,6 +512,11 @@ DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES = (
"services/device-manager",
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
)
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES = (
DEVICE_PLANE_MANAGER_COMPOSE_REL,
"services/device-manager",
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
)
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
"deployment/device-manager-control-plane-reconciliation-v1.json"
)
@@ -3922,6 +3947,7 @@ def allowed_payload_path(component, rel):
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
@@ -9494,6 +9520,7 @@ def is_device_plane_manager_control_plane_slice(component, entries):
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
)
)
@@ -9514,6 +9541,21 @@ def is_device_plane_manager_release_v3_slice(component, entries):
)
def is_device_plane_manager_release_v4_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES
)
def is_device_plane_manager_only_release_slice(component, entries):
return (
is_device_plane_manager_release_v3_slice(component, entries)
or is_device_plane_manager_release_v4_slice(component, entries)
)
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
return (
component == "device-plane"
@@ -9702,6 +9744,31 @@ def expected_device_plane_manager_release_v3_boundaries():
}
def expected_device_plane_manager_release_v4_boundaries():
return {
**expected_device_plane_manager_release_v3_boundaries(),
"predecessor": {
"kind": "release",
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
"artifactSha256": (
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256
),
},
"healthGate": "bounded-container-grace+core-contract+persistent-data",
"presentationPersistence": "runner-managed-host-data-bind",
"presentationDataHostPath": str(DEVICE_PLANE_MANAGER_DATA_DIR),
"presentationDataContainerPath": DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR,
"presentationDataOwnership": "uid-1000-gid-1000-mode-0750",
"presentationDataLifecycle": (
"preserve-across-manager-recreate-and-source-rollback"
),
"presentationPath": DEVICE_PLANE_MANAGER_PRESENTATION_PATH,
"mediaRoot": DEVICE_PLANE_MANAGER_MEDIA_ROOT,
"defaultAccentHex": "#f5f5f5",
"rollback": "restore-preapply-snapshot-preserve-manager-data",
}
def expected_device_plane_manager_release_boundaries():
# Compatibility name for the current release builder/tests. Immutable v1
# predecessors always use expected_device_plane_manager_release_v1_boundaries.
@@ -10216,6 +10283,17 @@ def validate_device_plane_manager_release_payload_contract(
"device-edge-channel/peers",
"name: nodedc-device-plane-egress",
))
if schema_version == "nodedc.device-plane.device-manager-release.v4":
required_compose.extend((
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: "
f"{DEVICE_PLANE_MANAGER_PRESENTATION_PATH}",
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT: "
f"{DEVICE_PLANE_MANAGER_MEDIA_ROOT}",
f"source: {DEVICE_PLANE_MANAGER_DATA_DIR}",
f"target: {DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}",
"read_only: false",
"create_host_path: false",
))
for required in required_compose:
if required not in compose:
die(f"Device Manager control-plane boundary missing: {required}")
@@ -10277,6 +10355,22 @@ def validate_device_plane_manager_release_v3_payload(
)
def validate_device_plane_manager_release_v4_payload(
payload_dir,
*,
expected_release_id=None,
):
return validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
schema_version="nodedc.device-plane.device-manager-release.v4",
boundaries=expected_device_plane_manager_release_v4_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
edge_channel=False,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_release_payload(
payload_dir,
*,
@@ -10285,12 +10379,18 @@ def validate_device_plane_manager_release_payload(
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
v3 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
v4 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
present = [
path for path in (v1, v2, v3)
path for path in (v1, v2, v3, v4)
if path.exists() or path.is_symlink()
]
if len(present) != 1:
die("Device Manager release descriptor cardinality mismatch")
if present[0] == v4:
return validate_device_plane_manager_release_v4_payload(
payload_dir,
expected_release_id=expected_release_id,
)
if present[0] == v3:
return validate_device_plane_manager_release_v3_payload(
payload_dir,
@@ -10568,6 +10668,7 @@ def installed_device_plane_manager_compose_sha256():
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
v3 = root / DEVICE_PLANE_MANAGER_RELEASE_V3_REL
v4 = root / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
# A successful v2 overlay intentionally leaves the immutable v1 release
# descriptor as predecessor evidence. Prefer the highest installed
@@ -10575,6 +10676,19 @@ def installed_device_plane_manager_compose_sha256():
# the generation-specific Compose digest. During a failed v2 apply,
# rollback removes the candidate-only v2 descriptor before rebuilding the
# restored v1 runtime, so the same lookup follows the restored source.
if v4.exists() or v4.is_symlink():
descriptor = read_strict_json(
v4,
"installed Device Manager release v4 descriptor",
max_bytes=16 * 1024,
)
validate_device_plane_manager_release_descriptor(
descriptor,
schema_version="nodedc.device-plane.device-manager-release.v4",
boundaries=expected_device_plane_manager_release_v4_boundaries(),
)
return DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256
if v3.exists() or v3.is_symlink():
descriptor = read_strict_json(
v3,
@@ -12089,8 +12203,9 @@ def validate_device_plane_manager_reconciliation_evidence(payload_dir):
def validate_device_plane_manager_v3_active_baseline(descriptor):
if descriptor.get("schemaVersion") != (
"nodedc.device-plane.device-manager-release.v3"
if descriptor.get("schemaVersion") not in (
"nodedc.device-plane.device-manager-release.v3",
"nodedc.device-plane.device-manager-release.v4",
):
return None
@@ -12235,8 +12350,9 @@ def validate_device_plane_manager_v3_active_baseline(descriptor):
def device_plane_manager_preserved_runtime_health_services(descriptor):
if descriptor.get("schemaVersion") == (
"nodedc.device-plane.device-manager-release.v3"
if descriptor.get("schemaVersion") in (
"nodedc.device-plane.device-manager-release.v3",
"nodedc.device-plane.device-manager-release.v4",
):
return (
"device-control-core",
@@ -12368,6 +12484,7 @@ def validate_device_plane_manager_activation_predecessor(
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
)
):
die("Device Manager release predecessor type mismatch")
@@ -12382,6 +12499,8 @@ def validate_device_plane_manager_activation_predecessor(
DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES:
DEVICE_PLANE_MANAGER_RELEASE_V3_REL,
DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES:
DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
}.get(
tuple(predecessor_entries),
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
@@ -14744,6 +14863,7 @@ def validate_device_manager_control_plane_runtime(
*,
require_edge_channel=None,
core_network_mode=None,
require_persistent_data=None,
):
core_ids = device_plane_service_container_ids("device-control-core")
manager_ids = device_plane_service_container_ids("device-manager")
@@ -14758,6 +14878,13 @@ def validate_device_manager_control_plane_runtime(
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL
)
require_edge_channel = edge_descriptor.is_file() and not edge_descriptor.is_symlink()
if require_persistent_data is None:
data_descriptor = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
)
require_persistent_data = (
data_descriptor.is_file() and not data_descriptor.is_symlink()
)
expected_core_environment = {
"DEVICE_MANAGEMENT_API_ENABLED": "true",
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
@@ -14816,6 +14943,23 @@ def validate_device_manager_control_plane_runtime(
"/run/nodedc-secrets/management-core-token"
),
}
if require_persistent_data:
expected_manager_environment.update({
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH": (
DEVICE_PLANE_MANAGER_PRESENTATION_PATH
),
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT": (
DEVICE_PLANE_MANAGER_MEDIA_ROOT
),
})
elif any(
key in manager_environment
for key in (
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH",
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT",
)
):
die("Device Manager runtime unexpectedly owns persistent data")
if any(
manager_environment.get(key) != value
for key, value in expected_manager_environment.items()
@@ -14900,16 +15044,23 @@ def validate_device_manager_control_plane_runtime(
DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE
),
}
if require_persistent_data:
expected_manager_mounts[DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR] = (
DEVICE_PLANE_MANAGER_DATA_DIR
)
if set(manager_mounts) != set(expected_manager_mounts):
die("Device Manager mount set mismatch")
for destination, source in expected_manager_mounts.items():
mount = manager_mounts[destination]
persistent_mount = destination == DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR
if (
mount.get("Type") != "bind"
or mount.get("Source") != str(source)
or mount.get("RW") is not False
or mount.get("RW") is not persistent_mount
):
die("Device Manager secret mount mismatch")
die("Device Manager mount boundary mismatch")
if require_persistent_data:
validate_device_plane_manager_persistent_data_metadata()
ports = (manager.get("NetworkSettings") or {}).get("Ports") or {}
if any(bindings for bindings in ports.values()):
die("Device Manager host port publication is forbidden")
@@ -16564,7 +16715,7 @@ def component_services(component, entries=None):
if is_device_plane_control_core_release_slice(component, entries):
return ("device-control-core",)
if is_device_plane_manager_release_v3_slice(component, entries):
if is_device_plane_manager_only_release_slice(component, entries):
return ("device-manager",)
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
@@ -17164,7 +17315,7 @@ def component_builds(component, entries=None):
),
),)
if is_device_plane_manager_release_v3_slice(component, entries):
if is_device_plane_manager_only_release_slice(component, entries):
return ((
DEVICE_PLANE_ROOT / "services/device-manager",
(
@@ -21096,7 +21247,10 @@ def plan_artifact(artifact):
"build+recreate:device-manager"
if device_plane_manager_activation_preflight["descriptor"].get(
"schemaVersion"
) == "nodedc.device-plane.device-manager-release.v3"
) in (
"nodedc.device-plane.device-manager-release.v3",
"nodedc.device-plane.device-manager-release.v4",
)
else "build+recreate:device-control-core,device-manager"
)
)
@@ -21107,12 +21261,38 @@ def plan_artifact(artifact):
"device-postgres,device-backhaul-target"
if device_plane_manager_activation_preflight["descriptor"].get(
"schemaVersion"
) == "nodedc.device-plane.device-manager-release.v3"
) in (
"nodedc.device-plane.device-manager-release.v3",
"nodedc.device-plane.device-manager-release.v4",
)
else "preserved:device-gateway,device-postgres,"
"device-backhaul-target"
)
)
print("device_manager_health_gate=bounded-grace+contract")
manager_schema = device_plane_manager_activation_preflight[
"descriptor"
].get("schemaVersion")
print(
"device_manager_health_gate="
+ (
"bounded-grace+contract+persistent-data"
if manager_schema
== "nodedc.device-plane.device-manager-release.v4"
else "bounded-grace+contract"
)
)
if manager_schema == (
"nodedc.device-plane.device-manager-release.v4"
):
print(
"device_manager_persistent_data=runner-managed-preserved:"
f"{DEVICE_PLANE_MANAGER_DATA_DIR}"
)
print(
"device_manager_persistent_mount=read-write:"
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}"
)
print("device_manager_default_accent=#f5f5f5")
if device_plane_manager_activation_preflight["descriptor"].get(
"commandTransport"
) == "typed-service-ping-v1":
@@ -21125,7 +21305,13 @@ def plan_artifact(artifact):
print("device_gateway_tcp_9921=preserved:loopback-only")
print(
"device_plane_rollback="
"source+reconciled-baseline-runtime"
+ (
"source+reconciled-baseline-runtime+"
"persistent-manager-data-preserved"
if manager_schema
== "nodedc.device-plane.device-manager-release.v4"
else "source+reconciled-baseline-runtime"
)
)
if device_plane_control_core_release_preflight is not None:
core_release = device_plane_control_core_release_preflight
@@ -22689,6 +22875,47 @@ def ensure_device_plane_backhaul_target_state():
die("Device Plane backhaul runtime trust verification failed")
def validate_device_plane_manager_persistent_data_metadata():
try:
data_stat = DEVICE_PLANE_MANAGER_DATA_DIR.lstat()
except FileNotFoundError:
die("Device Manager persistent data directory is missing")
if (
stat.S_ISLNK(data_stat.st_mode)
or not stat.S_ISDIR(data_stat.st_mode)
or data_stat.st_uid != 1000
or data_stat.st_gid != 1000
or stat.S_IMODE(data_stat.st_mode) != 0o750
):
die("Device Manager persistent data directory boundary mismatch")
return "uid-1000-gid-1000-mode-0750"
def ensure_device_plane_manager_persistent_data():
data_parent = DEVICE_PLANE_MANAGER_DATA_DIR.parent
parent_created = False
try:
parent_stat = data_parent.lstat()
except FileNotFoundError:
data_parent.mkdir(mode=0o755)
parent_created = True
parent_stat = data_parent.lstat()
if stat.S_ISLNK(parent_stat.st_mode) or not stat.S_ISDIR(
parent_stat.st_mode
):
die("Device Manager persistent data parent is unsafe")
if parent_created:
os.chown(data_parent, 0, 0)
data_parent.chmod(0o755)
DEVICE_PLANE_MANAGER_DATA_DIR.mkdir(exist_ok=True)
data_stat = DEVICE_PLANE_MANAGER_DATA_DIR.lstat()
if stat.S_ISLNK(data_stat.st_mode) or not stat.S_ISDIR(data_stat.st_mode):
die("Device Manager persistent data path is unsafe")
os.chown(DEVICE_PLANE_MANAGER_DATA_DIR, 1000, 1000)
DEVICE_PLANE_MANAGER_DATA_DIR.chmod(0o750)
return validate_device_plane_manager_persistent_data_metadata()
def stop_and_remove_compose_services(component, services):
if not services:
return
@@ -22771,6 +22998,8 @@ def prepare_component_runtime(component, entries=None):
MAP_GATEWAY_SECRET_RE,
"Device Core Hub handoff",
)
if is_device_plane_manager_release_v4_slice(component, entries):
ensure_device_plane_manager_persistent_data()
if (
is_device_plane_control_core_release_slice(component, entries)
or is_device_plane_edge_core_channel_bootstrap_slice(
@@ -23093,7 +23322,7 @@ def component_healthchecks(component, entries=None, services=None):
if is_device_plane_manager_control_plane_slice(component, entries):
command_transport = (
"typed-service-ping-v1"
if is_device_plane_manager_release_v3_slice(component, entries)
if is_device_plane_manager_only_release_slice(component, entries)
else "disabled"
)
checks = ({
@@ -23107,7 +23336,7 @@ def component_healthchecks(component, entries=None, services=None):
"commandTransport": command_transport,
},
},)
if is_device_plane_manager_release_v3_slice(component, entries):
if is_device_plane_manager_only_release_slice(component, entries):
checks += ({
"url": "http://127.0.0.1:18080/healthz",
"headers": {"Host": "device.nodedc.ru"},
@@ -23866,7 +24095,7 @@ def run_healthchecks(component, entries=None, services=None):
if is_device_plane_manager_control_plane_slice(component, entries):
expected_services = (
("device-manager",)
if is_device_plane_manager_release_v3_slice(component, entries)
if is_device_plane_manager_only_release_slice(component, entries)
else ("device-control-core", "device-manager")
)
if tuple(services or ()) != expected_services:
@@ -23878,7 +24107,13 @@ def run_healthchecks(component, entries=None, services=None):
)
for check in component_healthchecks(component, entries, services):
healthcheck_url(check)
if is_device_plane_manager_release_v3_slice(component, entries):
if is_device_plane_manager_release_v4_slice(component, entries):
validate_device_manager_control_plane_runtime(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
elif is_device_plane_manager_release_v3_slice(component, entries):
validate_device_manager_control_plane_runtime(
require_edge_channel=True,
core_network_mode="private-egress",
@@ -3,6 +3,7 @@ import importlib.machinery
import importlib.util
import inspect
import json
import stat
import tempfile
import unittest
from pathlib import Path
@@ -28,6 +29,30 @@ RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase):
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,
@@ -295,6 +320,110 @@ class DevicePlaneRegistryTest(unittest.TestCase):
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_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,