Compare commits
7
Commits
26dbcfd262
...
87a1e64807
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87a1e64807 | ||
|
|
952290a49d | ||
|
|
6123a06527 | ||
|
|
12901e0e19 | ||
|
|
a6fb60d38f | ||
|
|
94e437e842 | ||
|
|
c1c9818e30 |
@@ -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,15 +22378,8 @@ 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,
|
||||||
@@ -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,6 +24363,21 @@ 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_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()
|
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):
|
||||||
@@ -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,10 +25599,15 @@ 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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user