From 952290a49db50b8eed9e7deb0b892a895fb92ec3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 22 Aug 2026 10:23:07 +0300 Subject: [PATCH] deploy: register Manager v5 landing parity --- infra/deploy-runner/nodedc-deploy | 141 ++++++++++++++++-- .../test_device_plane_registry.py | 54 +++++++ 2 files changed, 179 insertions(+), 16 deletions(-) diff --git a/infra/deploy-runner/nodedc-deploy b/infra/deploy-runner/nodedc-deploy index 26e341c..ca24163 100755 --- a/infra/deploy-runner/nodedc-deploy +++ b/infra/deploy-runner/nodedc-deploy @@ -260,6 +260,9 @@ DEVICE_PLANE_MANAGER_RELEASE_V3_REL = ( 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_COMPOSE_REL = "docker-compose.device-manager.yml" DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL = ( "deployment/device-edge-core-channel-bootstrap-v1.json" @@ -373,6 +376,12 @@ DEVICE_PLANE_MANAGER_RELEASE_V4_PREDECESSOR_PATCH_ID = ( 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_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = ( "device-edge-core-channel-bootstrap-20260812-018" ) @@ -434,6 +443,9 @@ DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 = ( 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_V2_COMPOSE_SHA256 = ( "369a2acf9c1a1030b9e1c6c366144b1eaf8900aef0ee59bb6bf23250b7b371b9" ) @@ -517,6 +529,11 @@ DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES = ( "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_RECONCILIATION_REL = ( "deployment/device-manager-control-plane-reconciliation-v1.json" ) @@ -3948,6 +3965,7 @@ def allowed_payload_path(component, 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_EDGE_CORE_CHANNEL_BOOTSTRAP_REL, DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL, DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL, @@ -9521,6 +9539,7 @@ def is_device_plane_manager_control_plane_slice(component, 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, ) ) @@ -9549,10 +9568,25 @@ def is_device_plane_manager_release_v4_slice(component, 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_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) + ) + + 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) + or is_device_plane_manager_persistent_release_slice(component, entries) ) @@ -9769,6 +9803,20 @@ def expected_device_plane_manager_release_v4_boundaries(): } +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_boundaries(): # Compatibility name for the current release builder/tests. Immutable v1 # predecessors always use expected_device_plane_manager_release_v1_boundaries. @@ -10283,7 +10331,10 @@ def validate_device_plane_manager_release_payload_contract( "device-edge-channel/peers", "name: nodedc-device-plane-egress", )) - if schema_version == "nodedc.device-plane.device-manager-release.v4": + if schema_version in ( + "nodedc.device-plane.device-manager-release.v4", + "nodedc.device-plane.device-manager-release.v5", + ): required_compose.extend(( "NODEDC_DEVICE_MANAGER_PRESENTATION_PATH: " f"{DEVICE_PLANE_MANAGER_PRESENTATION_PATH}", @@ -10371,6 +10422,22 @@ def validate_device_plane_manager_release_v4_payload( ) +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_release_payload( payload_dir, *, @@ -10380,12 +10447,18 @@ def validate_device_plane_manager_release_payload( 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 present = [ - path for path in (v1, v2, v3, v4) + path for path in (v1, v2, v3, v4, v5) if path.exists() or path.is_symlink() ] if len(present) != 1: die("Device Manager release descriptor cardinality mismatch") + 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, @@ -10669,6 +10742,7 @@ def installed_device_plane_manager_compose_sha256(): 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 # A successful v2 overlay intentionally leaves the immutable v1 release # descriptor as predecessor evidence. Prefer the highest installed @@ -10676,6 +10750,19 @@ def installed_device_plane_manager_compose_sha256(): # the generation-specific Compose digest. During a failed v2 apply, # rollback removes the candidate-only v2 descriptor before rebuilding the # restored v1 runtime, so the same lookup follows the restored source. + if 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, @@ -12206,6 +12293,7 @@ def validate_device_plane_manager_v3_active_baseline(descriptor): 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", ): return None @@ -12353,6 +12441,7 @@ def device_plane_manager_preserved_runtime_health_services(descriptor): 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", ): return ( "device-control-core", @@ -12485,6 +12574,7 @@ def validate_device_plane_manager_activation_predecessor( DEVICE_PLANE_MANAGER_RELEASE_V2_ENTRIES, DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES, DEVICE_PLANE_MANAGER_RELEASE_V4_ENTRIES, + DEVICE_PLANE_MANAGER_RELEASE_V5_ENTRIES, ) ): die("Device Manager release predecessor type mismatch") @@ -12501,6 +12591,8 @@ def validate_device_plane_manager_activation_predecessor( 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, }.get( tuple(predecessor_entries), DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, @@ -14879,11 +14971,13 @@ def validate_device_manager_control_plane_runtime( ) 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 + data_descriptors = ( + DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V5_REL, + DEVICE_PLANE_ROOT / DEVICE_PLANE_MANAGER_RELEASE_V4_REL, ) - require_persistent_data = ( - data_descriptor.is_file() and not data_descriptor.is_symlink() + 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", @@ -21250,6 +21344,7 @@ def plan_artifact(artifact): ) in ( "nodedc.device-plane.device-manager-release.v3", "nodedc.device-plane.device-manager-release.v4", + "nodedc.device-plane.device-manager-release.v5", ) else "build+recreate:device-control-core,device-manager" ) @@ -21264,6 +21359,7 @@ def plan_artifact(artifact): ) in ( "nodedc.device-plane.device-manager-release.v3", "nodedc.device-plane.device-manager-release.v4", + "nodedc.device-plane.device-manager-release.v5", ) else "preserved:device-gateway,device-postgres," "device-backhaul-target" @@ -21272,18 +21368,19 @@ def plan_artifact(artifact): 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", + ) print( "device_manager_health_gate=" + ( "bounded-grace+contract+persistent-data" - if manager_schema - == "nodedc.device-plane.device-manager-release.v4" + if manager_persistent else "bounded-grace+contract" ) ) - if manager_schema == ( - "nodedc.device-plane.device-manager-release.v4" - ): + if manager_persistent: print( "device_manager_persistent_data=runner-managed-preserved:" f"{DEVICE_PLANE_MANAGER_DATA_DIR}" @@ -21293,6 +21390,13 @@ def plan_artifact(artifact): f"{DEVICE_PLANE_MANAGER_DATA_CONTAINER_DIR}" ) print("device_manager_default_accent=#f5f5f5") + if manager_schema == ( + "nodedc.device-plane.device-manager-release.v5" + ): + print( + "device_manager_overview_layout=" + "mission-core-landing-stage-v1" + ) if device_plane_manager_activation_preflight["descriptor"].get( "commandTransport" ) == "typed-service-ping-v1": @@ -21308,8 +21412,7 @@ def plan_artifact(artifact): + ( "source+reconciled-baseline-runtime+" "persistent-manager-data-preserved" - if manager_schema - == "nodedc.device-plane.device-manager-release.v4" + if manager_persistent else "source+reconciled-baseline-runtime" ) ) @@ -22998,7 +23101,10 @@ def prepare_component_runtime(component, entries=None): MAP_GATEWAY_SECRET_RE, "Device Core Hub handoff", ) - if is_device_plane_manager_release_v4_slice(component, entries): + 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) @@ -24107,7 +24213,10 @@ def run_healthchecks(component, entries=None, services=None): ) for check in component_healthchecks(component, entries, services): healthcheck_url(check) - if is_device_plane_manager_release_v4_slice(component, entries): + 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", diff --git a/infra/deploy-runner/test_device_plane_registry.py b/infra/deploy-runner/test_device_plane_registry.py index 3094457..afd007e 100644 --- a/infra/deploy-runner/test_device_plane_registry.py +++ b/infra/deploy-runner/test_device_plane_registry.py @@ -29,6 +29,32 @@ RUNNER = load_runner() class DevicePlaneRegistryTest(unittest.TestCase): + 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, @@ -348,6 +374,34 @@ class DevicePlaneRegistryTest(unittest.TestCase): 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_v4_prepare_owns_new_parent_and_managed_data_directory(self): with tempfile.TemporaryDirectory( prefix="nodedc-manager-v4-data-",