deploy: register Manager v4 persistent data

This commit is contained in:
Codex
2026-08-22 09:47:57 +03:00
parent 12901e0e19
commit 6123a06527
2 changed files with 381 additions and 17 deletions
+252 -17
View File
@@ -66,6 +66,14 @@ PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env")
DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime") 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,9 @@ 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_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"
@@ -356,6 +367,12 @@ 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_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 +431,9 @@ 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_V2_COMPOSE_SHA256 = ( DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 = (
"369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9" "369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9"
) )
@@ -492,6 +512,11 @@ 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_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 +3947,7 @@ 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_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 +9520,7 @@ 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,
) )
) )
@@ -9514,6 +9541,21 @@ def is_device_plane_manager_release_v3_slice(component, entries):
) )
def is_device_plane_manager_release_v4_slice(component, entries):
return (
component == "device-plane"
and entries is not None
and tuple(entries) == DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES
)
def is_device_plane_manager_only_release_slice(component, entries):
return (
is_device_plane_manager_release_v3_slice(component, entries)
or is_device_plane_manager_release_v4_slice(component, entries)
)
def is_device_plane_edge_core_channel_bootstrap_slice(component, entries): def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
return ( return (
component == "device-plane" component == "device-plane"
@@ -9702,6 +9744,31 @@ def expected_device_plane_manager_release_v3_boundaries():
} }
def expected_device_plane_manager_release_v4_boundaries():
return {
**expected_device_plane_manager_release_v3_boundaries(),
"predecessor": {
"kind": "release",
"patchId": DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
"artifactSha256": (
DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256
),
},
"healthGate": "bounded-container-grace+core-contract+persistent-data",
"presentationPersistence": "runner-managed-host-data-bind",
"presentationDataHostPath": str(DEVICE_PLANE_MANAGER_DATA_DIR),
"presentationDataContainerPath": DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR,
"presentationDataOwnership": "uid-1000-gid-1000-mode-0750",
"presentationDataLifecycle": (
"preserve-across-manager-recreate-and-source-rollback"
),
"presentationPath": DEVICE_PLANE_MANAGER_PRESENTATION_PATH,
"mediaRoot": DEVICE_PLANE_MANAGER_MEDIA_ROOT,
"defaultAccentHex": "#f5f5f5",
"rollback": "restore-preapply-snapshot-preserve-manager-data",
}
def expected_device_plane_manager_release_boundaries(): 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 +10283,17 @@ 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 == "nodedc.device-plane.device-manager-release.v4":
required_compose.extend((
"NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: "
f"{DEVICE_PLANE_MANAGER_PRESENTATION_PATH}",
"NODEDC_DEVICE_MANAGER_MEDIA_ROOT: "
f"{DEVICE_PLANE_MANAGER_MEDIA_ROOT}",
f"source: {DEVICE_PLANE_MANAGER_DATA_DIR}",
f"target: {DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}",
"read_only: false",
"create_host_path: false",
))
for required in required_compose: 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 +10355,22 @@ def validate_device_plane_manager_release_v3_payload(
) )
def validate_device_plane_manager_release_v4_payload(
payload_dir,
*,
expected_release_id=None,
):
return validate_device_plane_manager_release_payload_contract(
payload_dir,
descriptor_rel=DEVICE_PLANE_MANAGER_RELEASE_V4_REL,
schema_version="nodedc.device-plane.device-manager-release.v4",
boundaries=expected_device_plane_manager_release_v4_boundaries(),
compose_sha256=DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
edge_channel=False,
expected_release_id=expected_release_id,
)
def validate_device_plane_manager_release_payload( def validate_device_plane_manager_release_payload(
payload_dir, payload_dir,
*, *,
@@ -10285,12 +10379,18 @@ 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
present = [ present = [
path for path in (v1, v2, v3) path for path in (v1, v2, v3, v4)
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] == 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 +10668,7 @@ 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
# 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 +10676,19 @@ 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 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 +12203,9 @@ 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",
): ):
return None return None
@@ -12235,8 +12350,9 @@ 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",
): ):
return ( return (
"device-control-core", "device-control-core",
@@ -12368,6 +12484,7 @@ def validate_device_plane_manager_activation_predecessor(
DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES, DEVICE_PLANE_MANAGER_RELEASE_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,
) )
): ):
die("Device Manager release predecessor type mismatch") die("Device Manager release predecessor type mismatch")
@@ -12382,6 +12499,8 @@ def validate_device_plane_manager_activation_predecessor(
DEVICE_PLANE_MANAGER_RELEASE_V2_REL, DEVICE_PLANE_MANAGER_RELEASE_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,
}.get( }.get(
tuple(predecessor_entries), tuple(predecessor_entries),
DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL,
@@ -14744,6 +14863,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")
@@ -14758,6 +14878,13 @@ 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_descriptor = (
DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V4_REL
)
require_persistent_data = (
data_descriptor.is_file() and not data_descriptor.is_symlink()
)
expected_core_environment = { expected_core_environment = {
"DEVICE_MANAGEMENT_API_ENABLED": "true", "DEVICE_MANAGEMENT_API_ENABLED": "true",
"DEVICE_MANAGEMENT_CORE_TOKEN_FILE": ( "DEVICE_MANAGEMENT_CORE_TOKEN_FILE": (
@@ -14816,6 +14943,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()
@@ -14900,16 +15044,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")
@@ -16564,7 +16715,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):
@@ -17164,7 +17315,7 @@ def component_builds(component, entries=None):
), ),
),) ),)
if is_device_plane_manager_release_v3_slice(component, entries): if is_device_plane_manager_only_release_slice(component, entries):
return (( return ((
DEVICE_PLANE_ROOT / "services/device-manager", DEVICE_PLANE_ROOT / "services/device-manager",
( (
@@ -21096,7 +21247,10 @@ 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",
)
else "build+recreate:device-control-core,device-manager" else "build+recreate:device-control-core,device-manager"
) )
) )
@@ -21107,12 +21261,38 @@ 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",
)
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")
print(
"device_manager_health_gate="
+ (
"bounded-grace+contract+persistent-data"
if manager_schema
== "nodedc.device-plane.device-manager-release.v4"
else "bounded-grace+contract"
)
)
if manager_schema == (
"nodedc.device-plane.device-manager-release.v4"
):
print(
"device_manager_persistent_data=runner-managed-preserved:"
f"{DEVICE_PLANE_MANAGER_DATA_DIR}"
)
print(
"device_manager_persistent_mount=read-write:"
f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}"
)
print("device_manager_default_accent=#f5f5f5")
if device_plane_manager_activation_preflight["descriptor"].get( if device_plane_manager_activation_preflight["descriptor"].get(
"commandTransport" "commandTransport"
) == "typed-service-ping-v1": ) == "typed-service-ping-v1":
@@ -21125,7 +21305,13 @@ 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_schema
== "nodedc.device-plane.device-manager-release.v4"
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
@@ -22689,6 +22875,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
@@ -22771,6 +22998,8 @@ 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_release_v4_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(
@@ -23093,7 +23322,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 = ({
@@ -23107,7 +23336,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"},
@@ -23866,7 +24095,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:
@@ -23878,7 +24107,13 @@ 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)
if is_device_plane_manager_release_v3_slice(component, entries): if is_device_plane_manager_release_v4_slice(component, entries):
validate_device_manager_control_plane_runtime(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
elif is_device_plane_manager_release_v3_slice(component, entries):
validate_device_manager_control_plane_runtime( validate_device_manager_control_plane_runtime(
require_edge_channel=True, require_edge_channel=True,
core_network_mode="private-egress", core_network_mode="private-egress",
@@ -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,30 @@ RUNNER = load_runner()
class DevicePlaneRegistryTest(unittest.TestCase): class DevicePlaneRegistryTest(unittest.TestCase):
def test_manager_v4_pins_032_and_persistent_white_boundary(self):
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID,
"device-manager-release-v3-20260822-032",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_ARTIFACT_SHA256,
"6e0eb3a0a6f19ceab92d46832b93bffbcea21247dbdc2ea50625a51ff460e4ca",
)
self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_COMPOSE_SHA256,
"e7dff0f5873ad4586bd55946d3db2bb86092a5e149e886d120adc041e056c256",
)
boundaries = RUNNER.expected_device_plane_manager_release_v4_boundaries()
self.assertEqual(boundaries["defaultAccentHex"], "#f5f5f5")
self.assertEqual(
boundaries["presentationDataHostPath"],
"/volume1/docker/nodedc-device-plane/data/device-manager",
)
self.assertEqual(
boundaries["presentationDataContainerPath"],
"/var/lib/nodedc-device-manager",
)
def test_manager_v3_targets_applied_control_core_recovery(self): def test_manager_v3_targets_applied_control_core_recovery(self):
self.assertEqual( self.assertEqual(
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID, RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_CONTROL_CORE_PREDECESSOR_PATCH_ID,
@@ -295,6 +320,110 @@ class DevicePlaneRegistryTest(unittest.TestCase):
core_network_mode="private-egress", core_network_mode="private-egress",
) )
def test_manager_v4_post_apply_requires_persistent_data(self):
with (
mock.patch.object(
RUNNER,
"healthcheck_compose_service_with_grace",
),
mock.patch.object(
RUNNER,
"component_healthchecks",
return_value=(),
),
mock.patch.object(
RUNNER,
"validate_device_manager_control_plane_runtime",
) as runtime_acceptance,
):
RUNNER.run_healthchecks(
"device-plane",
RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES,
("device-manager",),
)
runtime_acceptance.assert_called_once_with(
require_edge_channel=True,
core_network_mode="private-egress",
require_persistent_data=True,
)
def test_manager_v4_prepare_owns_new_parent_and_managed_data_directory(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-data-",
) as directory:
data_dir = Path(directory) / "data" / "device-manager"
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_dir,
),
mock.patch.object(RUNNER.os, "chown") as chown,
mock.patch.object(
RUNNER,
"validate_device_plane_manager_persistent_data_metadata",
return_value="uid-1000-gid-1000-mode-0750",
) as validate,
):
result = RUNNER.ensure_device_plane_manager_persistent_data()
self.assertEqual(result, "uid-1000-gid-1000-mode-0750")
self.assertEqual(
chown.call_args_list,
[
mock.call(data_dir.parent, 0, 0),
mock.call(data_dir, 1000, 1000),
],
)
validate.assert_called_once_with()
def test_manager_v4_prepare_preserves_existing_safe_parent_metadata(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-parent-",
) as directory:
data_parent = Path(directory) / "data"
data_parent.mkdir(mode=0o700)
data_dir = data_parent / "device-manager"
with (
mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_dir,
),
mock.patch.object(RUNNER.os, "chown") as chown,
mock.patch.object(
RUNNER,
"validate_device_plane_manager_persistent_data_metadata",
return_value="uid-1000-gid-1000-mode-0750",
),
):
RUNNER.ensure_device_plane_manager_persistent_data()
self.assertEqual(stat.S_IMODE(data_parent.stat().st_mode), 0o700)
chown.assert_called_once_with(data_dir, 1000, 1000)
def test_manager_v4_prepare_rejects_symlink_parent_before_child_creation(self):
with tempfile.TemporaryDirectory(
prefix="nodedc-manager-v4-symlink-",
) as directory:
root = Path(directory)
outside = root / "outside"
outside.mkdir()
data_parent = root / "data"
data_parent.symlink_to(outside, target_is_directory=True)
with mock.patch.object(
RUNNER,
"DEVICE_PLANE_MANAGER_DATA_DIR",
data_parent / "device-manager",
):
with self.assertRaisesRegex(
RUNNER.DeployError,
"persistent data parent is unsafe",
):
RUNNER.ensure_device_plane_manager_persistent_data()
self.assertFalse((outside / "device-manager").exists())
def test_manager_predecessor_health_is_phase_scoped(self): def test_manager_predecessor_health_is_phase_scoped(self):
with self.assertRaisesRegex( with self.assertRaisesRegex(
RUNNER.DeployError, RUNNER.DeployError,