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") DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime")
DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane") DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane")
DEVICE_PLANE_SECRET_DIR = DEVICE_PLANE_ROOT / "secrets" 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_POSTGRES_PASSWORD_FILE = DEVICE_PLANE_SECRET_DIR / "postgres-password"
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE = DEVICE_PLANE_SECRET_DIR / "gateway-core-token" DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE = DEVICE_PLANE_SECRET_DIR / "gateway-core-token"
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepper" 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 = ( DEVICE_PLANE_MANAGER_RELEASE_V3_REL = (
"deployment/device-manager-release-v3.json" "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_MANAGER_COMPOSE_REL = "docker-compose.device-manager.yml"
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = ( DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = (
"deployment/device-edge-core-channel-bootstrap-v1.json" "deployment/device-edge-core-channel-bootstrap-v1.json"
@@ -345,10 +362,10 @@ DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 = (
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" "c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc"
) )
DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID = ( 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 = ( DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_ARTIFACT_SHA256 = (
"816ef07fd81923328c3ff54d833dcc49f873aef13256259ae4f66061e7453635" "8459521a662541a5a87cb0188991cdcfb51727427db8ec2a232ce4846bfc3454"
) )
DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = ( DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-upgrade-v4-20260812-023" "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 = ( DEVICE_PLANE_MANAGER_RELEASE_V3_EDGE_CHANNEL_PREDECESSOR_ARTIFACT_SHA256 = (
"c10d5b6b7d55ab239f85b6c8130e34ce9f84985e3b46e6e5534733156c7982fc" "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_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
"device-edge-core-channel-bootstrap-20260812-018" "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 = ( DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 = (
"4954120aaddc999798b64c304d8cf692b79714feb727d873117bd1f3434e865e" "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 = ( DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9" "369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
) )
@@ -492,6 +559,21 @@ DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES = (
"services/device-manager", "services/device-manager",
DEVICE_PLANE_MANAGER_RELEASE_V3_REL, 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 = ( DEVICE_PLANE_MANAGER_RECONCILIATION_REL = (
"deployment/device-manager-control-plane-reconciliation-v1.json" "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_CONTROL_PLANE_REL,
DEVICE_PLANE_MANAGER_RELEASE_V2_REL, DEVICE_PLANE_MANAGER_RELEASE_V2_REL,
DEVICE_PLANE_MANAGER_RELEASE_V3_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_BOOTSTRAP_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL, DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_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_V1_SUCCESSOR_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES, DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V3_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): def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
return ( return (
component == "device-plane" 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(): def expected_device_plane_manager_release_boundaries():
# Compatibility name for the current release builder/tests. Immutable v1 # Compatibility name for the current release builder/tests. Immutable v1
# predecessors always use expected_device_plane_manager_release_v1_boundaries. # 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", "device-edge-channel/peers",
"name: nodedc-device-plane-egress", "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: for required in required_compose:
if required not in compose: if required not in compose:
die(f"Device Manager control-plane boundary missing: {required}") 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( def validate_device_plane_manager_release_payload(
payload_dir, payload_dir,
*, *,
@@ -10285,12 +10562,30 @@ def validate_device_plane_manager_release_payload(
v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL v1 = payload_dir / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL v2 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
v3 = payload_dir / DEVICE_PLANE_MANAGER_RELEASE_V3_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 = [ 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 path.exists() or path.is_symlink()
] ]
if len(present) != 1: if len(present) != 1:
die("Device Manager release descriptor cardinality mismatch") 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: if present[0] == v3:
return validate_device_plane_manager_release_v3_payload( return validate_device_plane_manager_release_v3_payload(
payload_dir, payload_dir,
@@ -10568,6 +10863,9 @@ def installed_device_plane_manager_compose_sha256():
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL v2 = root / DEVICE_PLANE_MANAGER_RELEASE_V2_REL
v3 = root / DEVICE_PLANE_MANAGER_RELEASE_V3_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 # A successful v2 overlay intentionally leaves the immutable v1 release
# descriptor as predecessor evidence. Prefer the highest installed # 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, # the generation-specific Compose digest. During a failed v2 apply,
# rollback removes the candidate-only v2 descriptor before rebuilding the # rollback removes the candidate-only v2 descriptor before rebuilding the
# restored v1 runtime, so the same lookup follows the restored source. # 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(): if v3.exists() or v3.is_symlink():
descriptor = read_strict_json( descriptor = read_strict_json(
v3, v3,
@@ -12089,8 +12426,11 @@ def validate_device_plane_manager_reconciliation_evidence(payload_dir):
def validate_device_plane_manager_v3_active_baseline(descriptor): def validate_device_plane_manager_v3_active_baseline(descriptor):
if descriptor.get("schemaVersion") != ( if descriptor.get("schemaVersion") not in (
"nodedc.device-plane.device-manager-release.v3" "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 return None
@@ -12235,8 +12575,11 @@ def validate_device_plane_manager_v3_active_baseline(descriptor):
def device_plane_manager_preserved_runtime_health_services(descriptor): def device_plane_manager_preserved_runtime_health_services(descriptor):
if descriptor.get("schemaVersion") == ( if descriptor.get("schemaVersion") in (
"nodedc.device-plane.device-manager-release.v3" "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 ( return (
"device-control-core", "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_V1_SUCCESSOR_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES, DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES,
DEVICE_PLANE_MANAGER_RELEASE_V3_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") 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_V2_REL,
DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES: DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES:
DEVICE_PLANE_MANAGER_RELEASE_V3_REL, 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( }.get(
tuple(predecessor_entries), tuple(predecessor_entries),
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, 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( descriptor = validate_device_plane_control_core_release_payload(
payload_dir 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() identity_state = inspect_device_edge_channel_core_identity_state()
if identity_state != "valid-reuse-at-apply": if identity_state != "valid-reuse-at-apply":
die("Device Control Core release requires the valid active identity") die("Device Control Core release requires the valid active identity")
for service in ( selected_runtime = (
"device-control-core", validate_device_plane_control_core_selected_predecessor_runtime()
"device-manager", )
"device-gateway", preserved_runtime_health = "deferred-to-apply"
"device-postgres", if preflight_phase == "apply":
): preserved_runtime_health = (
healthcheck_compose_service("device-plane", service) validate_device_plane_control_core_preserved_runtime_health(
descriptor
)
)
validate_device_manager_control_plane_runtime( validate_device_manager_control_plane_runtime(
require_edge_channel=True, require_edge_channel=True,
core_network_mode="private-egress", core_network_mode="private-egress",
@@ -12997,6 +13380,8 @@ def validate_device_plane_control_core_release_predecessor(payload_dir):
"descriptor": descriptor, "descriptor": descriptor,
"identityState": identity_state, "identityState": identity_state,
"predecessorArtifact": artifact, "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 ( or item.get("service") not in (
*DEVICE_PLANE_RUNTIME_SERVICES, *DEVICE_PLANE_RUNTIME_SERVICES,
"device-manager", "device-manager",
DEVICE_PLANE_BACKHAUL_TARGET_SERVICE,
) )
or not isinstance(item.get("containerId"), str) or not isinstance(item.get("containerId"), str)
or not re.fullmatch(r"[a-f0-9]{64}", item["containerId"]) 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) 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): def validate_device_plane_foundation_recovery_evidence(payload_dir):
backup_dir = BACKUPS_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID backup_dir = BACKUPS_DIR / DEVICE_PLANE_FOUNDATION_RECOVERY_BACKUP_ID
try: try:
@@ -14627,6 +15096,7 @@ def validate_device_manager_control_plane_runtime(
*, *,
require_edge_channel=None, require_edge_channel=None,
core_network_mode=None, core_network_mode=None,
require_persistent_data=None,
): ):
core_ids = device_plane_service_container_ids("device-control-core") core_ids = device_plane_service_container_ids("device-control-core")
manager_ids = device_plane_service_container_ids("device-manager") 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 DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL
) )
require_edge_channel = edge_descriptor.is_file() and not edge_descriptor.is_symlink() 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 = { expected_core_environment = {
"DEVICE_MANAGEMENT_API_ENABLED": "true", "DEVICE_MANAGEMENT_API_ENABLED": "true",
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": ( "DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
@@ -14699,6 +15179,23 @@ def validate_device_manager_control_plane_runtime(
"/run/nodedc-secrets/management-core-token" "/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( if any(
manager_environment.get(key) != value manager_environment.get(key) != value
for key, value in expected_manager_environment.items() 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 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): if set(manager_mounts) != set(expected_manager_mounts):
die("Device Manager mount set mismatch") die("Device Manager mount set mismatch")
for destination, source in expected_manager_mounts.items(): for destination, source in expected_manager_mounts.items():
mount = manager_mounts[destination] mount = manager_mounts[destination]
persistent_mount = destination == DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR
if ( if (
mount.get("Type") != "bind" mount.get("Type") != "bind"
or mount.get("Source") != str(source) 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 {} ports = (manager.get("NetworkSettings") or {}).get("Ports") or {}
if any(bindings for bindings in ports.values()): if any(bindings for bindings in ports.values()):
die("Device Manager host port publication is forbidden") 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): if is_device_plane_control_core_release_slice(component, entries):
return ("device-control-core",) 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",) return ("device-manager",)
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries): 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 (( return ((
DEVICE_PLANE_ROOT / "services/device-manager", DEVICE_PLANE_ROOT / "services/device-manager",
( (
@@ -19354,7 +19858,8 @@ def plan_artifact(artifact):
): ):
device_plane_control_core_release_preflight = ( device_plane_control_core_release_preflight = (
validate_device_plane_control_core_release_predecessor( validate_device_plane_control_core_release_predecessor(
payload_dir payload_dir,
preflight_phase="plan",
) )
) )
if is_device_plane_manager_reconciliation_slice( if is_device_plane_manager_reconciliation_slice(
@@ -20978,7 +21483,12 @@ def plan_artifact(artifact):
"build+recreate:device-manager" "build+recreate:device-manager"
if device_plane_manager_activation_preflight["descriptor"].get( if device_plane_manager_activation_preflight["descriptor"].get(
"schemaVersion" "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" else "build+recreate:device-control-core,device-manager"
) )
) )
@@ -20989,12 +21499,54 @@ def plan_artifact(artifact):
"device-postgres,device-backhaul-target" "device-postgres,device-backhaul-target"
if device_plane_manager_activation_preflight["descriptor"].get( if device_plane_manager_activation_preflight["descriptor"].get(
"schemaVersion" "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," else "preserved:device-gateway,device-postgres,"
"device-backhaul-target" "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( if device_plane_manager_activation_preflight["descriptor"].get(
"commandTransport" "commandTransport"
) == "typed-service-ping-v1": ) == "typed-service-ping-v1":
@@ -21007,7 +21559,12 @@ def plan_artifact(artifact):
print("device_gateway_tcp_9921=preserved:loopback-only") print("device_gateway_tcp_9921=preserved:loopback-only")
print( print(
"device_plane_rollback=" "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: if device_plane_control_core_release_preflight is not None:
core_release = device_plane_control_core_release_preflight core_release = device_plane_control_core_release_preflight
@@ -21821,16 +22378,9 @@ def rollback_device_plane_apply(
"device-plane", "device-plane",
entries, entries,
): ):
for service in ( accept_device_plane_control_core_rollback_runtime(
"device-control-core", runtime_inventory
"device-manager", )
"device-gateway",
"device-postgres",
):
healthcheck_compose_service_with_grace(
"device-plane",
service,
)
validate_device_manager_control_plane_runtime( validate_device_manager_control_plane_runtime(
require_edge_channel=True, require_edge_channel=True,
core_network_mode="private-egress", core_network_mode="private-egress",
@@ -22578,6 +23128,47 @@ def ensure_device_plane_backhaul_target_state():
die("Device Plane backhaul runtime trust verification failed") 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): def stop_and_remove_compose_services(component, services):
if not services: if not services:
return return
@@ -22660,6 +23251,11 @@ def prepare_component_runtime(component, entries=None):
MAP_GATEWAY_SECRET_RE, MAP_GATEWAY_SECRET_RE,
"Device Core Hub handoff", "Device Core Hub handoff",
) )
if is_device_plane_manager_persistent_release_slice(
component,
entries,
):
ensure_device_plane_manager_persistent_data()
if ( if (
is_device_plane_control_core_release_slice(component, entries) is_device_plane_control_core_release_slice(component, entries)
or is_device_plane_edge_core_channel_bootstrap_slice( 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): if is_device_plane_manager_control_plane_slice(component, entries):
command_transport = ( command_transport = (
"typed-service-ping-v1" "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" else "disabled"
) )
checks = ({ checks = ({
@@ -22996,7 +23592,7 @@ def component_healthchecks(component, entries=None, services=None):
"commandTransport": command_transport, "commandTransport": command_transport,
}, },
},) },)
if is_device_plane_manager_release_v3_slice(component, entries): if is_device_plane_manager_only_release_slice(component, entries):
checks += ({ checks += ({
"url": "http://127.0.0.1:18080/healthz", "url": "http://127.0.0.1:18080/healthz",
"headers": {"Host": "device.nodedc.ru"}, "headers": {"Host": "device.nodedc.ru"},
@@ -23595,6 +24191,7 @@ def run_healthchecks(component, entries=None, services=None):
"device-manager", "device-manager",
"device-gateway", "device-gateway",
"device-postgres", "device-postgres",
"device-backhaul-target",
): ):
healthcheck_compose_service_with_grace( healthcheck_compose_service_with_grace(
"device-plane", "device-plane",
@@ -23754,7 +24351,7 @@ def run_healthchecks(component, entries=None, services=None):
if is_device_plane_manager_control_plane_slice(component, entries): if is_device_plane_manager_control_plane_slice(component, entries):
expected_services = ( expected_services = (
("device-manager",) ("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") else ("device-control-core", "device-manager")
) )
if tuple(services or ()) != expected_services: 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): for check in component_healthchecks(component, entries, services):
healthcheck_url(check) 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 return
if is_device_plane_edge_core_channel_bootstrap_slice(component, entries): if is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
if tuple(services or ()) != ("device-control-core",): if tuple(services or ()) != ("device-control-core",):
@@ -24659,7 +25271,8 @@ def apply_artifact(artifact):
entries, entries,
): ):
validate_device_plane_control_core_release_predecessor( validate_device_plane_control_core_release_predecessor(
payload_dir payload_dir,
preflight_phase="apply",
) )
if is_device_plane_manager_reconciliation_slice( if is_device_plane_manager_reconciliation_slice(
component, component,
@@ -24986,11 +25599,16 @@ def apply_artifact(artifact):
component, component,
entries, 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, component,
entries, entries,
) ):
):
inventory_services = ( inventory_services = (
*inventory_services, *inventory_services,
"device-manager", "device-manager",
@@ -3,6 +3,7 @@ import importlib.machinery
import importlib.util import importlib.util
import inspect import inspect
import json import json
import stat
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -28,6 +29,510 @@ RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase): 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): def test_manager_predecessor_health_is_phase_scoped(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
RUNNER.DeployError, RUNNER.DeployError,