7 Commits
2 changed files with 1165 additions and 42 deletions
+660 -42
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,15 @@ 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_RELEASE_V5_REL = (
"deployment/device-manager-release-v5.json"
)
DEVICE_PLANE_MANAGER_RELEASE_V6_REL = (
"deployment/device-manager-release-v6.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"
@@ -345,10 +362,10 @@ DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 = (
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
)
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID = (
"device-control-core-release-v2-20260813-028"
"device-control-core-release-v2-20260821-030"
)
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
"816ef07fd81923328c3ff54d833dcc49f873aef13256259ae4f66061e7453635"
"8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
)
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-upgrade-v4-20260812-023"
@@ -356,6 +373,24 @@ 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_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID = (
"device-manager-release-v4-20260822-033"
)
DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256 = (
"52ba322042f1e4f595bbfea99f8bb35630b15984e0da648dc55348bc9e5b2066"
)
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID = (
"device-manager-release-v5-20260822-034"
)
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256 = (
"acc1d2ae2cda66861054826928c25d01a2428e688cc8132a9c381831bf29ab5a"
)
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-bootstrap-20260812-018"
)
@@ -414,6 +449,38 @@ 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_V5_COMPOSE_SHA256 = (
DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256
)
DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256 = (
DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256
)
DEVICE_PLANE_MANAGER_FAVICON_SHA256 = {
"favicon.ico": (
"f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2"
),
"favicon/favicon.ico": (
"f8933114a85646335ea5c94944f56d3cd8c48a6032016719f7905ff244ec0aa2"
),
"favicon/icon-adaptive.svg": (
"481984e83997d786bb0a72ad1ee80037db13aef3a0792ab3109df95c2199b38e"
),
"favicon/apple-touch-icon.png": (
"afdccc28152a566e264e533ca218362f05d5bcec936c647f9a54f414c0bd4763"
),
"favicon/icon-192.png": (
"5b10a24feb4754f15c69761cef42f91a01f885d04095156b1a12e254875fdd4d"
),
"favicon/icon-512.png": (
"f98bac3dba59b7eefbe89f8bb8abc25567226a54ab7ed3b6b1a4caffbdd9ee15"
),
"favicon/manifest.webmanifest.json": (
"2a8ecdc6e6c64833f812ae02bbc0c7bd9b435e0cfa75cc21d6edf054d41275fc"
),
}
DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
)
@@ -492,6 +559,21 @@ 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_RELEASE_V5_ENTRIES = (
DEVICE_PLANE_MANAGER_COMPOSE_REL,
"services/device-manager",
DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
)
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES = (
DEVICE_PLANE_MANAGER_COMPOSE_REL,
"services/device-manager",
DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
)
DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
"deployment/device-manager-control-plane-reconciliation-v1.json"
)
@@ -3922,6 +4004,9 @@ 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_MANAGER_RELEASE_V5_REL,
DEVICE_PLANE_MANAGER_RELEASE_V6_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 +9579,9 @@ 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,
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
)
)
@@ -9514,6 +9602,45 @@ 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_release_v5_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES
)
def is_device_plane_manager_release_v6_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES
)
def is_device_plane_manager_persistent_release_slice(component, entries):
return (
is_device_plane_manager_release_v4_slice(component, entries)
or is_device_plane_manager_release_v5_slice(component, entries)
or is_device_plane_manager_release_v6_slice(component, 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_persistent_release_slice(component, entries)
)
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
return (
component == "device-plane"
@@ -9702,6 +9829,59 @@ 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_v5_boundaries():
return {
**expected_device_plane_manager_release_v4_boundaries(),
"predecessor": {
"kind": "release",
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_PATCH_ID,
"artifactSha256": (
DEVICE_PLANE_MANAGER_RELEASE_V5_PREDECESSOR_ARTIFACT_SHA256
),
},
"overviewLayout": "mission-core-landing-stage-v1",
}
def expected_device_plane_manager_release_v6_boundaries():
return {
**expected_device_plane_manager_release_v5_boundaries(),
"predecessor": {
"kind": "release",
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_PATCH_ID,
"artifactSha256": (
DEVICE_PLANE_MANAGER_RELEASE_V6_PREDECESSOR_ARTIFACT_SHA256
),
},
"faviconSet": "nodedc-adaptive-v1",
}
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 +10396,21 @@ def validate_device_plane_manager_release_payload_contract(
"device-edge-channel/peers",
"name: nodedc-device-plane-egress",
))
if schema_version in (
"nodedc.device-plane.device-manager-release.v4",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
):
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 +10472,88 @@ 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_v5_payload(
payload_dir,
*,
expected_release_id=None,
):
return validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
schema_version="nodedc.device-plane.device-manager-release.v5",
boundaries=expected_device_plane_manager_release_v5_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256,
edge_channel=False,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_v6_favicon_bundle(payload_dir):
dist = payload_dir / "services/device-manager/dist"
for relative_path, expected_sha256 in (
DEVICE_PLANE_MANAGER_FAVICON_SHA256.items()
):
path = dist / relative_path
if (
not path.is_file()
or path.is_symlink()
or sha256_file(path) != expected_sha256
):
die(
"Device Manager canonical favicon mismatch: "
f"{relative_path}"
)
index_path = dist / "index.html"
if not index_path.is_file() or index_path.is_symlink():
die("Device Manager favicon HTML entrypoint mismatch")
index_html = index_path.read_text(encoding="utf-8")
for required_link in (
'href="/favicon/icon-adaptive.svg"',
'href="/favicon/favicon.ico"',
'href="/favicon/apple-touch-icon.png"',
'href="/favicon/icon-192.png"',
'href="/favicon/icon-512.png"',
'href="/favicon/manifest.webmanifest.json"',
):
if required_link not in index_html:
die(f"Device Manager favicon link missing: {required_link}")
return "nodedc-adaptive-v1"
def validate_device_plane_manager_release_v6_payload(
payload_dir,
*,
expected_release_id=None,
):
descriptor = validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
schema_version="nodedc.device-plane.device-manager-release.v6",
boundaries=expected_device_plane_manager_release_v6_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256,
edge_channel=False,
expected_release_id=expected_release_id,
)
validate_device_plane_manager_v6_favicon_bundle(payload_dir)
return descriptor
def validate_device_plane_manager_release_payload(
payload_dir,
*,
@@ -10285,12 +10562,30 @@ 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
v5 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V5_REL
v6 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V6_REL
present = [
path for path in (v1, v2, v3)
path for path in (v1, v2, v3, v4, v5, v6)
if path.exists() or path.is_symlink()
]
if len(present) != 1:
die("Device Manager release descriptor cardinality mismatch")
if present[0] == v6:
return validate_device_plane_manager_release_v6_payload(
payload_dir,
expected_release_id=expected_release_id,
)
if present[0] == v5:
return validate_device_plane_manager_release_v5_payload(
payload_dir,
expected_release_id=expected_release_id,
)
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 +10863,9 @@ 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
v5 = root / DEVICE_PLANE_MANAGER_RELEASE_V5_REL
v6 = root / DEVICE_PLANE_MANAGER_RELEASE_V6_REL
# A successful v2 overlay intentionally leaves the immutable v1 release
# descriptor as predecessor evidence. Prefer the highest installed
@@ -10575,6 +10873,45 @@ 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 v6.exists() or v6.is_symlink():
descriptor = read_strict_json(
v6,
"installed Device Manager release v6 descriptor",
max_bytes=16 * 1024,
)
validate_device_plane_manager_release_descriptor(
descriptor,
schema_version="nodedc.device-plane.device-manager-release.v6",
boundaries=expected_device_plane_manager_release_v6_boundaries(),
)
return DEVICE_PLANE_MANAGER_RELEASE_V6_COMPOSE_SHA256
if v5.exists() or v5.is_symlink():
descriptor = read_strict_json(
v5,
"installed Device Manager release v5 descriptor",
max_bytes=16 * 1024,
)
validate_device_plane_manager_release_descriptor(
descriptor,
schema_version="nodedc.device-plane.device-manager-release.v5",
boundaries=expected_device_plane_manager_release_v5_boundaries(),
)
return DEVICE_PLANE_MANAGER_RELEASE_V5_COMPOSE_SHA256
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 +12426,11 @@ 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",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
):
return None
@@ -12235,8 +12575,11 @@ 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",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
):
return (
"device-control-core",
@@ -12368,6 +12711,9 @@ 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,
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES,
)
):
die("Device Manager release predecessor type mismatch")
@@ -12382,6 +12728,12 @@ 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,
DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES:
DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
DEVICE_PLANE_MANAGER_RELEASE_V6_ENTRIES:
DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
}.get(
tuple(predecessor_entries),
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
@@ -12877,7 +13229,35 @@ def validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
}
def validate_device_plane_control_core_release_predecessor(payload_dir):
def device_plane_control_core_preserved_runtime_health_services(descriptor):
services = tuple(descriptor.get("preservedServices") or ())
expected = (
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
)
if services != expected:
die("Device Control Core preserved service set mismatch")
return services
def validate_device_plane_control_core_preserved_runtime_health(descriptor):
services = device_plane_control_core_preserved_runtime_health_services(
descriptor
)
for service in services:
healthcheck_compose_service("device-plane", service)
return services
def validate_device_plane_control_core_release_predecessor(
payload_dir,
*,
preflight_phase,
):
if preflight_phase not in ("plan", "apply"):
die("Device Control Core predecessor preflight phase is invalid")
descriptor = validate_device_plane_control_core_release_payload(
payload_dir
)
@@ -12981,13 +13361,16 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
identity_state = inspect_device_edge_channel_core_identity_state()
if identity_state != "valid-reuse-at-apply":
die("Device Control Core release requires the valid active identity")
for service in (
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
):
healthcheck_compose_service("device-plane", service)
selected_runtime = (
validate_device_plane_control_core_selected_predecessor_runtime()
)
preserved_runtime_health = "deferred-to-apply"
if preflight_phase == "apply":
preserved_runtime_health = (
validate_device_plane_control_core_preserved_runtime_health(
descriptor
)
)
validate_device_manager_control_plane_runtime(
require_edge_channel=True,
core_network_mode="private-egress",
@@ -12997,6 +13380,8 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
"descriptor": descriptor,
"identityState": identity_state,
"predecessorArtifact": artifact,
"selectedRuntime": selected_runtime,
"preservedRuntimeHealth": preserved_runtime_health,
}
@@ -13287,6 +13672,7 @@ def device_plane_inventory_service_names(inventory):
or item.get("service") not in (
*DEVICE_PLANE_RUNTIME_SERVICES,
"device-manager",
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
)
or not isinstance(item.get("containerId"), str)
or not re.fullmatch(r"[a-f0-9]{64}", item["containerId"])
@@ -13311,6 +13697,89 @@ def device_plane_inventory_service_names(inventory):
return tuple(names)
def validate_device_plane_control_core_selected_predecessor_runtime():
inventory = device_plane_runtime_inventory(("device-control-core",))
names = device_plane_inventory_service_names(inventory)
if names != ("device-control-core",):
die("Device Control Core selected predecessor runtime is missing")
# This is the selected repair target, not a preserved dependency. Its
# observed Docker state is evidence for rollback only; health and state
# acceptance belongs to the new generation after recreate.
return inventory["services"][0]
def accept_device_plane_control_core_rollback_runtime(runtime_before):
expected_services = (
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
)
names = device_plane_inventory_service_names(runtime_before)
if set(names) != set(expected_services):
die("Device Control Core rollback predecessor inventory mismatch")
before = {
item["service"]: item
for item in runtime_before["services"]
}
selected_before = before["device-control-core"]
predecessor_was_healthy = (
selected_before["status"] == "running"
and selected_before["running"] is True
and selected_before["health"] == "healthy"
)
for service in expected_services[1:]:
healthcheck_compose_service_with_grace("device-plane", service)
container_id = compose_service_container_id(
"device-plane",
"device-control-core",
)
if container_id == selected_before["containerId"]:
die("Device Control Core rollback generation was not recreated")
last_status = "unknown"
for attempt in range(1, 61):
container = inspect_device_plane_container(container_id)
state = container.get("State") or {}
health = (state.get("Health") or {}).get("Status")
last_status = health or state.get("Status") or "unknown"
restored_healthy = (
state.get("Status") == "running"
and state.get("Running") is True
and health == "healthy"
)
restored_repair_boundary = (
state.get("Status") in (
"created",
"running",
"paused",
"restarting",
"exited",
)
and isinstance(state.get("Running"), bool)
and health in (None, "starting", "healthy", "unhealthy")
)
if restored_healthy or (
not predecessor_was_healthy
and restored_repair_boundary
):
return {
"containerId": container_id,
"health": health,
"status": state.get("Status"),
"predecessorHealth": selected_before["health"],
"predecessorStatus": selected_before["status"],
}
if attempt < 60:
time.sleep(5)
die(
"Device Control Core rollback state did not converge: "
f"{last_status}"
)
def validate_device_plane_foundation_recovery_evidence(payload_dir):
backup_dir = BACKUPS_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID
try:
@@ -14627,6 +15096,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")
@@ -14641,6 +15111,16 @@ 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_descriptors = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V6_REL,
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V5_REL,
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
)
require_persistent_data = any(
descriptor.is_file() and not descriptor.is_symlink()
for descriptor in data_descriptors
)
expected_core_environment = {
"DEVICE_MANAGEMENT_API_ENABLED": "true",
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
@@ -14699,6 +15179,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()
@@ -14783,16 +15280,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")
@@ -16447,7 +16951,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):
@@ -17047,7 +17551,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",
(
@@ -19354,7 +19858,8 @@ def plan_artifact(artifact):
):
device_plane_control_core_release_preflight = (
validate_device_plane_control_core_release_predecessor(
payload_dir
payload_dir,
preflight_phase="plan",
)
)
if is_device_plane_manager_reconciliation_slice(
@@ -20978,7 +21483,12 @@ 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",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
)
else "build+recreate:device-control-core,device-manager"
)
)
@@ -20989,12 +21499,54 @@ 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",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
)
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")
manager_persistent = manager_schema in (
"nodedc.device-plane.device-manager-release.v4",
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
)
print(
"device_manager_health_gate="
+ (
"bounded-grace+contract+persistent-data"
if manager_persistent
else "bounded-grace+contract"
)
)
if manager_persistent:
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 manager_schema in (
"nodedc.device-plane.device-manager-release.v5",
"nodedc.device-plane.device-manager-release.v6",
):
print(
"device_manager_overview_layout="
"mission-core-landing-stage-v1"
)
if manager_schema == (
"nodedc.device-plane.device-manager-release.v6"
):
print("device_manager_favicon_set=nodedc-adaptive-v1")
if device_plane_manager_activation_preflight["descriptor"].get(
"commandTransport"
) == "typed-service-ping-v1":
@@ -21007,7 +21559,12 @@ 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_persistent
else "source+reconciled-baseline-runtime"
)
)
if device_plane_control_core_release_preflight is not None:
core_release = device_plane_control_core_release_preflight
@@ -21821,16 +22378,9 @@ def rollback_device_plane_apply(
"device-plane",
entries,
):
for service in (
"device-control-core",
"device-manager",
"device-gateway",
"device-postgres",
):
healthcheck_compose_service_with_grace(
"device-plane",
service,
)
accept_device_plane_control_core_rollback_runtime(
runtime_inventory
)
validate_device_manager_control_plane_runtime(
require_edge_channel=True,
core_network_mode="private-egress",
@@ -22578,6 +23128,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
@@ -22660,6 +23251,11 @@ def prepare_component_runtime(component, entries=None):
MAP_GATEWAY_SECRET_RE,
"Device Core Hub handoff",
)
if is_device_plane_manager_persistent_release_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(
@@ -22982,7 +23578,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 = ({
@@ -22996,7 +23592,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"},
@@ -23595,6 +24191,7 @@ def run_healthchecks(component, entries=None, services=None):
"device-manager",
"device-gateway",
"device-postgres",
"device-backhaul-target",
):
healthcheck_compose_service_with_grace(
"device-plane",
@@ -23754,7 +24351,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:
@@ -23766,7 +24363,22 @@ def run_healthchecks(component, entries=None, services=None):
)
for check in component_healthchecks(component, entries, services):
healthcheck_url(check)
validate_device_manager_control_plane_runtime()
if is_device_plane_manager_persistent_release_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",
)
else:
validate_device_manager_control_plane_runtime()
return
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
if tuple(services or ()) != ("device-control-core",):
@@ -24659,7 +25271,8 @@ def apply_artifact(artifact):
entries,
):
validate_device_plane_control_core_release_predecessor(
payload_dir
payload_dir,
preflight_phase="apply",
)
if is_device_plane_manager_reconciliation_slice(
component,
@@ -24986,11 +25599,16 @@ def apply_artifact(artifact):
component,
entries,
)
or is_device_plane_edge_core_channel_bootstrap_slice(
):
inventory_services = (
*inventory_services,
"device-manager",
"device-backhaul-target",
)
elif is_device_plane_edge_core_channel_bootstrap_slice(
component,
entries,
)
):
):
inventory_services = (
*inventory_services,
"device-manager",
@@ -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,510 @@ RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase):
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,