fix(device-edge): validate Core egress transition
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"schemaVersion": "nodedc.device-plane.device-edge-core-channel-upgrade.v4",
|
||||
"transitionId": "__PATCH_ID__",
|
||||
"action": "upgrade",
|
||||
"upgradePredecessor": {
|
||||
"patchId": "device-edge-core-channel-upgrade-v2-20260812-021",
|
||||
"artifactSha256": "e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
|
||||
},
|
||||
"failedAttempt": {
|
||||
"patchId": "device-edge-core-channel-upgrade-v3-20260812-022",
|
||||
"artifactSha256": "9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613",
|
||||
"backupId": "device-plane-device-edge-core-channel-upgrade-v3-20260812-022-20260812-123620"
|
||||
},
|
||||
"service": "device-control-core",
|
||||
"composeActivation": "replace-core-network-membership-with-private-plus-egress",
|
||||
"identity": "reuse-existing-runner-managed-host-local-private-key-public-certificate-export",
|
||||
"identityRecovery": "forbidden-valid-existing-identity-required",
|
||||
"tlsPurpose": "clientAuth",
|
||||
"direction": "core-initiated",
|
||||
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
||||
"coreNetworks": [
|
||||
"device-plane-private",
|
||||
"device-plane-egress"
|
||||
],
|
||||
"removedCoreNetwork": "device-plane-control",
|
||||
"composeCompatibility": "synology-compose-v2.20-no-gw-priority",
|
||||
"publicIngress": "none-on-synology",
|
||||
"edgeRegistrations": "preserved",
|
||||
"commandTransport": "disabled",
|
||||
"gelios": "untouched",
|
||||
"preservedServices": [
|
||||
"device-manager",
|
||||
"device-gateway",
|
||||
"device-postgres",
|
||||
"device-backhaul-target"
|
||||
],
|
||||
"healthGate": "bounded-container-grace+core-edge-contract+exact-private-egress-network-boundary",
|
||||
"rollback": "restore-upgrade-v2-021-source-and-preapply-core-runtime"
|
||||
}
|
||||
@@ -70,7 +70,6 @@ services:
|
||||
- "127.0.0.1:18120:18120"
|
||||
networks:
|
||||
- device-plane-private
|
||||
- device-plane-control
|
||||
depends_on:
|
||||
device-postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -15,19 +15,25 @@ if (extra.length || !/^[A-Za-z0-9._-]{1,96}$/.test(patchId)) {
|
||||
throw new Error("usage: build-device-edge-core-channel-bootstrap-artifact.mjs [patch-id]");
|
||||
}
|
||||
|
||||
const upgradeV2 = patchId.startsWith("device-edge-core-channel-upgrade-v2-");
|
||||
const upgradeV1 = !upgradeV2 && patchId.startsWith("device-edge-core-channel-upgrade-");
|
||||
const upgrade = upgradeV1 || upgradeV2;
|
||||
const descriptorPath = upgradeV2
|
||||
? "deployment/device-edge-core-channel-upgrade-v2.json"
|
||||
const upgradeV4 = patchId.startsWith("device-edge-core-channel-upgrade-v4-");
|
||||
const upgradeV2 = !upgradeV4 && patchId.startsWith("device-edge-core-channel-upgrade-v2-");
|
||||
const upgradeV1 = !upgradeV4 && !upgradeV2 && patchId.startsWith("device-edge-core-channel-upgrade-");
|
||||
const upgrade = upgradeV1 || upgradeV2 || upgradeV4;
|
||||
const descriptorPath = upgradeV4
|
||||
? "deployment/device-edge-core-channel-upgrade-v4.json"
|
||||
: upgradeV2
|
||||
? "deployment/device-edge-core-channel-upgrade-v2.json"
|
||||
: upgradeV1
|
||||
? "deployment/device-edge-core-channel-upgrade-v1.json"
|
||||
: "deployment/device-edge-core-channel-bootstrap-v1.json";
|
||||
const composePath = upgradeV4
|
||||
? "docker-compose.device-plane.yml"
|
||||
: "docker-compose.device-edge-core-channel.yml";
|
||||
const entries = [
|
||||
".dockerignore",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"docker-compose.device-edge-core-channel.yml",
|
||||
composePath,
|
||||
"packages/device-protocol-contract",
|
||||
"packages/device-edge-channel-contract",
|
||||
"services/device-control-core",
|
||||
@@ -73,39 +79,82 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
const compose = await readFile(join(payload, "docker-compose.device-edge-core-channel.yml"), "utf8");
|
||||
for (const required of [
|
||||
const compose = await readFile(join(payload, composePath), "utf8");
|
||||
const commonComposeRequired = [
|
||||
"device-control-core:",
|
||||
"DEVICE_EDGE_CHANNEL_ENABLED: \"true\"",
|
||||
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: /run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
||||
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: /run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
||||
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: /run/nodedc-secrets/device-edge-channel/peers",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers",
|
||||
"name: nodedc-device-plane-egress",
|
||||
]) {
|
||||
];
|
||||
const composeRequired = upgradeV4
|
||||
? [
|
||||
...commonComposeRequired,
|
||||
" - device-plane-private",
|
||||
" - device-plane-control",
|
||||
]
|
||||
: [
|
||||
...commonComposeRequired,
|
||||
"DEVICE_EDGE_CHANNEL_ENABLED: \"true\"",
|
||||
"DEVICE_EDGE_CHANNEL_CORE_KEY_FILE: /run/nodedc-secrets/device-edge-channel/core-private-key.pem",
|
||||
"DEVICE_EDGE_CHANNEL_CORE_CERTIFICATE_FILE: /run/nodedc-secrets/device-edge-channel/core-certificate.pem",
|
||||
"DEVICE_EDGE_CHANNEL_TRUST_ROOT: /run/nodedc-secrets/device-edge-channel/peers",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-private-key.pem",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/core-certificate.pem",
|
||||
"source: /volume1/docker/nodedc-device-plane/secrets/device-edge-channel/peers",
|
||||
"name: nodedc-device-plane-egress",
|
||||
];
|
||||
for (const required of composeRequired) {
|
||||
if (!compose.includes(required)) {
|
||||
throw new Error(`device_edge_core_channel_compose_contract_missing:${required}`);
|
||||
}
|
||||
}
|
||||
for (const forbidden of [
|
||||
"device-manager:",
|
||||
"device-gateway:",
|
||||
"device-postgres:",
|
||||
"PRIVATE KEY",
|
||||
"ports:",
|
||||
"network_mode:",
|
||||
"privileged:",
|
||||
]) {
|
||||
if (upgradeV4) {
|
||||
const coreBlock = compose.split("\n device-control-core:", 2)[1]?.split("\n device-gateway:", 1)[0] || "";
|
||||
const gatewayBlock = compose.split("\n device-gateway:", 2)[1]?.split("\nnetworks:", 1)[0] || "";
|
||||
if (
|
||||
!coreBlock.includes(" - device-plane-private")
|
||||
|| coreBlock.includes(" - device-plane-control")
|
||||
|| !gatewayBlock.includes(" - device-plane-private")
|
||||
|| !gatewayBlock.includes(" - device-plane-control")
|
||||
) {
|
||||
throw new Error("device_edge_core_channel_v4_network_boundary_mismatch");
|
||||
}
|
||||
}
|
||||
const composeForbidden = upgradeV4
|
||||
? ["gw_priority:", "network_mode:", "privileged:"]
|
||||
: [
|
||||
"device-manager:",
|
||||
"device-gateway:",
|
||||
"device-postgres:",
|
||||
"PRIVATE KEY",
|
||||
"ports:",
|
||||
"network_mode:",
|
||||
"privileged:",
|
||||
];
|
||||
for (const forbidden of composeForbidden) {
|
||||
if (compose.includes(forbidden)) {
|
||||
throw new Error(`device_edge_core_channel_compose_boundary_violation:${forbidden}`);
|
||||
}
|
||||
}
|
||||
|
||||
const descriptor = JSON.parse(await readFile(join(payload, descriptorPath), "utf8"));
|
||||
const descriptorContractMatches = upgradeV2
|
||||
const descriptorContractMatches = upgradeV4
|
||||
? (
|
||||
descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-upgrade.v4"
|
||||
&& descriptor.action === "upgrade"
|
||||
&& descriptor.composeActivation === "replace-core-network-membership-with-private-plus-egress"
|
||||
&& descriptor.identityRecovery === "forbidden-valid-existing-identity-required"
|
||||
&& descriptor.endpointPolicy === "public-ipv4-standard-https-tcp-443-only"
|
||||
&& descriptor.upgradePredecessor?.patchId === "device-edge-core-channel-upgrade-v2-20260812-021"
|
||||
&& descriptor.upgradePredecessor?.artifactSha256 === "e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
|
||||
&& descriptor.failedAttempt?.patchId === "device-edge-core-channel-upgrade-v3-20260812-022"
|
||||
&& descriptor.failedAttempt?.artifactSha256 === "9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613"
|
||||
&& descriptor.failedAttempt?.backupId === "device-plane-device-edge-core-channel-upgrade-v3-20260812-022-20260812-123620"
|
||||
&& JSON.stringify(descriptor.coreNetworks) === JSON.stringify(["device-plane-private", "device-plane-egress"])
|
||||
&& descriptor.removedCoreNetwork === "device-plane-control"
|
||||
&& descriptor.composeCompatibility === "synology-compose-v2.20-no-gw-priority"
|
||||
&& descriptor.edgeRegistrations === "preserved"
|
||||
&& descriptor.rollback === "restore-upgrade-v2-021-source-and-preapply-core-runtime"
|
||||
)
|
||||
: upgradeV2
|
||||
? (
|
||||
descriptor.schemaVersion === "nodedc.device-plane.device-edge-core-channel-upgrade.v2"
|
||||
&& descriptor.action === "upgrade"
|
||||
&& descriptor.composeActivation === "preserve-dedicated-additive-override"
|
||||
|
||||
@@ -256,12 +256,21 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL = (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL = (
|
||||
"deployment/device-edge-core-channel-upgrade-v2.json"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL = (
|
||||
"deployment/device-edge-core-channel-upgrade-v4.json"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL = (
|
||||
"docker-compose.device-edge-core-channel.yml"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256 = (
|
||||
"cac1e06b21202d8d96f5694b1adb1e67c0a3cdd31fcfca56cb7e19839c8516d8"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256 = (
|
||||
"eb1018cc0ffeaa0c019944d8810e2daa01eeca4c06289efff175785fb16457e7"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256 = (
|
||||
"6a4c08313cc97bbfd86c4e133c6b2e55ba18a499a3bb6f267c7059754f83e8cf"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES = (
|
||||
".dockerignore",
|
||||
"package.json",
|
||||
@@ -292,6 +301,16 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES = (
|
||||
"services/device-control-core",
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES = (
|
||||
".dockerignore",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"docker-compose.device-plane.yml",
|
||||
"packages/device-protocol-contract",
|
||||
"packages/device-edge-channel-contract",
|
||||
"services/device-control-core",
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID = (
|
||||
"device-edge-core-channel-bootstrap-20260812-018"
|
||||
)
|
||||
@@ -304,6 +323,26 @@ DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID = (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256 = (
|
||||
"8e9a220275959f378c1c4b00be5c7192e79afe2134eaab808a64e515870a8438"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID = (
|
||||
"device-edge-core-channel-upgrade-v2-20260812-021"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256 = (
|
||||
"e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID = (
|
||||
"device-edge-core-channel-upgrade-v3-20260812-022"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256 = (
|
||||
"9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT = (
|
||||
"nodedc-device-plane-device-edge-core-channel-upgrade-v3-"
|
||||
"20260812-022.tgz.20260812-123620"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID = (
|
||||
"device-plane-device-edge-core-channel-upgrade-v3-"
|
||||
"20260812-022-20260812-123620"
|
||||
)
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_MANAGER_PREDECESSOR_PATCH_ID = (
|
||||
"device-manager-release-20260811-010"
|
||||
)
|
||||
@@ -3835,6 +3874,7 @@ def allowed_payload_path(component, rel):
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_REL,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_REL,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||
DEVICE_PLANE_MANAGER_RECONCILIATION_REL,
|
||||
DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL,
|
||||
"packages/device-protocol-contract",
|
||||
@@ -8891,7 +8931,15 @@ def load_artifact(artifact, work_dir):
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
if is_device_plane_edge_core_channel_upgrade_v2_slice(
|
||||
if is_device_plane_edge_core_channel_upgrade_v4_slice(
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
||||
payload_dir,
|
||||
expected_transition_id=manifest["id"],
|
||||
)
|
||||
elif is_device_plane_edge_core_channel_upgrade_v2_slice(
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
@@ -9404,6 +9452,7 @@ def is_device_plane_edge_core_channel_bootstrap_slice(component, entries):
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9415,6 +9464,7 @@ def is_device_plane_edge_core_channel_upgrade_slice(component, entries):
|
||||
and tuple(entries) in (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -9427,6 +9477,14 @@ def is_device_plane_edge_core_channel_upgrade_v2_slice(component, entries):
|
||||
)
|
||||
|
||||
|
||||
def is_device_plane_edge_core_channel_upgrade_v4_slice(component, entries):
|
||||
return (
|
||||
component == "device-plane"
|
||||
and entries is not None
|
||||
and tuple(entries) == DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
||||
)
|
||||
|
||||
|
||||
def is_device_plane_manager_v2_control_plane_slice(component, entries):
|
||||
return (
|
||||
component == "device-plane"
|
||||
@@ -9661,6 +9719,72 @@ def expected_device_plane_edge_core_channel_upgrade_v2_descriptor(
|
||||
}
|
||||
|
||||
|
||||
def expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
transition_id,
|
||||
):
|
||||
return {
|
||||
"schemaVersion": (
|
||||
"nodedc.device-plane.device-edge-core-channel-upgrade.v4"
|
||||
),
|
||||
"transitionId": transition_id,
|
||||
"action": "upgrade",
|
||||
"upgradePredecessor": {
|
||||
"patchId": (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID
|
||||
),
|
||||
"artifactSha256": (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256
|
||||
),
|
||||
},
|
||||
"failedAttempt": {
|
||||
"patchId": (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID
|
||||
),
|
||||
"artifactSha256": (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256
|
||||
),
|
||||
"backupId": (
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID
|
||||
),
|
||||
},
|
||||
"service": "device-control-core",
|
||||
"composeActivation": (
|
||||
"replace-core-network-membership-with-private-plus-egress"
|
||||
),
|
||||
"identity": (
|
||||
"reuse-existing-runner-managed-host-local-private-key-"
|
||||
"public-certificate-export"
|
||||
),
|
||||
"identityRecovery": "forbidden-valid-existing-identity-required",
|
||||
"tlsPurpose": "clientAuth",
|
||||
"direction": "core-initiated",
|
||||
"endpointPolicy": "public-ipv4-standard-https-tcp-443-only",
|
||||
"coreNetworks": [
|
||||
"device-plane-private",
|
||||
"device-plane-egress",
|
||||
],
|
||||
"removedCoreNetwork": "device-plane-control",
|
||||
"composeCompatibility": "synology-compose-v2.20-no-gw-priority",
|
||||
"publicIngress": "none-on-synology",
|
||||
"edgeRegistrations": "preserved",
|
||||
"commandTransport": "disabled",
|
||||
"gelios": "untouched",
|
||||
"preservedServices": [
|
||||
"device-manager",
|
||||
"device-gateway",
|
||||
"device-postgres",
|
||||
"device-backhaul-target",
|
||||
],
|
||||
"healthGate": (
|
||||
"bounded-container-grace+core-edge-contract+"
|
||||
"exact-private-egress-network-boundary"
|
||||
),
|
||||
"rollback": (
|
||||
"restore-upgrade-v2-021-source-and-preapply-core-runtime"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def expected_device_plane_manager_failed_control_plane_descriptor():
|
||||
return {
|
||||
"schemaVersion": (
|
||||
@@ -10048,6 +10172,36 @@ def validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
||||
return descriptor
|
||||
|
||||
|
||||
def validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
||||
payload_dir,
|
||||
*,
|
||||
expected_transition_id=None,
|
||||
):
|
||||
descriptor = read_strict_json(
|
||||
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||
"Device Edge Core channel upgrade v4 descriptor",
|
||||
max_bytes=16 * 1024,
|
||||
)
|
||||
transition_id = descriptor.get("transitionId")
|
||||
if (
|
||||
not isinstance(transition_id, str)
|
||||
or not re.fullmatch(r"[A-Za-z0-9._-]{1,96}", transition_id)
|
||||
or (
|
||||
expected_transition_id is not None
|
||||
and transition_id != expected_transition_id
|
||||
)
|
||||
or descriptor
|
||||
!= expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
transition_id
|
||||
)
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 descriptor mismatch")
|
||||
validate_device_plane_edge_core_channel_upgrade_v4_base_compose(
|
||||
payload_dir
|
||||
)
|
||||
return descriptor
|
||||
|
||||
|
||||
def validate_device_plane_edge_core_channel_compose(payload_dir):
|
||||
compose = payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
|
||||
if sha256_file(compose) != DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_SHA256:
|
||||
@@ -10087,6 +10241,38 @@ def validate_device_plane_edge_core_channel_compose(payload_dir):
|
||||
return compose
|
||||
|
||||
|
||||
def validate_device_plane_edge_core_channel_upgrade_v4_base_compose(
|
||||
payload_dir,
|
||||
):
|
||||
compose = payload_dir / "docker-compose.device-plane.yml"
|
||||
if (
|
||||
sha256_file(compose)
|
||||
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256
|
||||
):
|
||||
die("Device Edge Core channel v4 base Compose mismatch")
|
||||
compose_text = compose.read_text(encoding="utf-8")
|
||||
try:
|
||||
core_block = compose_text.split(
|
||||
"\n device-control-core:", 1
|
||||
)[1].split("\n device-gateway:", 1)[0]
|
||||
gateway_block = compose_text.split(
|
||||
"\n device-gateway:", 1
|
||||
)[1].split("\nnetworks:", 1)[0]
|
||||
except IndexError:
|
||||
die("Device Edge Core channel v4 service topology is incomplete")
|
||||
if (
|
||||
" - device-plane-private" not in core_block
|
||||
or " - device-plane-control" in core_block
|
||||
or " - device-plane-private" not in gateway_block
|
||||
or " - device-plane-control" not in gateway_block
|
||||
or "gw_priority:" in compose_text
|
||||
or "network_mode:" in compose_text
|
||||
or "privileged:" in compose_text
|
||||
):
|
||||
die("Device Edge Core channel v4 network boundary mismatch")
|
||||
return compose
|
||||
|
||||
|
||||
def installed_device_plane_manager_compose_sha256():
|
||||
root = DEVICE_PLANE_ROOT
|
||||
v1 = root / DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL
|
||||
@@ -11777,6 +11963,12 @@ def inspect_device_edge_channel_core_identity_state():
|
||||
def validate_device_plane_edge_core_channel_bootstrap_predecessor(
|
||||
payload_dir,
|
||||
):
|
||||
if (
|
||||
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
||||
).exists():
|
||||
return validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
||||
payload_dir
|
||||
)
|
||||
if (
|
||||
payload_dir / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
|
||||
).exists():
|
||||
@@ -12075,6 +12267,128 @@ def validate_device_plane_edge_core_channel_upgrade_v2_predecessor(
|
||||
}
|
||||
|
||||
|
||||
def validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
||||
payload_dir,
|
||||
):
|
||||
descriptor = validate_device_plane_edge_core_channel_upgrade_v4_payload(
|
||||
payload_dir
|
||||
)
|
||||
installed_upgrade = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
||||
)
|
||||
if installed_upgrade.exists() or installed_upgrade.is_symlink():
|
||||
die("Device Edge Core channel upgrade v4 is already installed")
|
||||
predecessor = descriptor["upgradePredecessor"]
|
||||
artifact_name = f"nodedc-device-plane-{predecessor['patchId']}.tgz"
|
||||
artifact = APPLIED_DIR / artifact_name
|
||||
if (
|
||||
not artifact.is_file()
|
||||
or artifact.is_symlink()
|
||||
or sha256_file(artifact) != predecessor["artifactSha256"]
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 predecessor mismatch")
|
||||
records = [
|
||||
row for row in load_state(STATE_FILE)
|
||||
if row.get("id") == predecessor["patchId"]
|
||||
and row.get("sha256") == predecessor["artifactSha256"]
|
||||
]
|
||||
if (
|
||||
len(records) != 1
|
||||
or records[0].get("status") != "ok"
|
||||
or records[0].get("component") != "device-plane"
|
||||
or records[0].get("artifact") != artifact_name
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 predecessor journal mismatch")
|
||||
failed = descriptor["failedAttempt"]
|
||||
failed_artifact = (
|
||||
FAILED_DIR / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT
|
||||
)
|
||||
failed_backup = BACKUPS_DIR / failed["backupId"]
|
||||
if (
|
||||
not failed_artifact.is_file()
|
||||
or failed_artifact.is_symlink()
|
||||
or sha256_file(failed_artifact) != failed["artifactSha256"]
|
||||
or not failed_backup.is_dir()
|
||||
or failed_backup.is_symlink()
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 failed-attempt evidence mismatch")
|
||||
failed_records = [
|
||||
row for row in load_state(FAILED_STATE_FILE)
|
||||
if row.get("id") == failed["patchId"]
|
||||
and row.get("sha256") == failed["artifactSha256"]
|
||||
]
|
||||
if (
|
||||
len(failed_records) != 1
|
||||
or failed_records[0].get("status") != "failed"
|
||||
or failed_records[0].get("component") != "device-plane"
|
||||
or failed_records[0].get("backup_id") != failed["backupId"]
|
||||
or failed_records[0].get("started_apply") is not True
|
||||
or failed_records[0].get("rollback_status")
|
||||
!= "ok:device-plane-overlay:source+runtime-restored:8"
|
||||
or failed_records[0].get("message")
|
||||
!= "Device Control Core Edge channel network boundary mismatch"
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 failed-attempt journal mismatch")
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="device-edge-core-channel-upgrade-v4-predecessor-",
|
||||
dir=TMP_DIR,
|
||||
) as directory:
|
||||
manifest, entries, predecessor_payload = load_artifact(
|
||||
artifact,
|
||||
Path(directory),
|
||||
)
|
||||
if (
|
||||
manifest.get("id") != predecessor["patchId"]
|
||||
or manifest.get("component") != "device-plane"
|
||||
or manifest.get("type") != "app-overlay"
|
||||
or tuple(entries)
|
||||
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES
|
||||
):
|
||||
die("Device Edge Core channel upgrade v4 predecessor type mismatch")
|
||||
validate_device_plane_edge_core_channel_upgrade_v2_payload(
|
||||
predecessor_payload,
|
||||
expected_transition_id=predecessor["patchId"],
|
||||
)
|
||||
expected_source = collect_exact_files(
|
||||
predecessor_payload,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
||||
"Device Edge Core channel upgrade v4 predecessor source",
|
||||
)
|
||||
actual_source = collect_exact_files(
|
||||
DEVICE_PLANE_ROOT,
|
||||
DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
||||
"installed Device Edge Core channel upgrade v2 source",
|
||||
)
|
||||
if actual_source != expected_source:
|
||||
die("installed Device Edge Core channel upgrade v2 source drift detected")
|
||||
installed_base_compose = DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
||||
if (
|
||||
installed_base_compose.is_symlink()
|
||||
or not installed_base_compose.is_file()
|
||||
or sha256_file(installed_base_compose)
|
||||
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256
|
||||
):
|
||||
die("installed Device Edge Core channel v4 base predecessor drift detected")
|
||||
identity_state = inspect_device_edge_channel_core_identity_state()
|
||||
if identity_state != "valid-reuse-at-apply":
|
||||
die("Device Edge Core channel upgrade v4 requires the valid active identity")
|
||||
for service in (
|
||||
"device-control-core",
|
||||
"device-manager",
|
||||
"device-gateway",
|
||||
"device-postgres",
|
||||
):
|
||||
healthcheck_compose_service("device-plane", service)
|
||||
return {
|
||||
"mode": "edge-core-channel-private-plus-egress-upgrade-v4",
|
||||
"descriptor": descriptor,
|
||||
"identityState": identity_state,
|
||||
"upgradeArtifact": artifact,
|
||||
"failedArtifact": failed_artifact,
|
||||
"failedBackup": failed_backup,
|
||||
}
|
||||
|
||||
|
||||
def validate_device_plane_manager_v2_reconciliation_backup():
|
||||
backup_dir = (
|
||||
BACKUPS_DIR / DEVICE_PLANE_MANAGER_V2_RECONCILIATION_BACKUP_ID
|
||||
@@ -13698,7 +14012,11 @@ def validate_device_plane_runtime_secret_metadata(include_management=False):
|
||||
return "exact"
|
||||
|
||||
|
||||
def validate_device_manager_control_plane_runtime(*, require_edge_channel=None):
|
||||
def validate_device_manager_control_plane_runtime(
|
||||
*,
|
||||
require_edge_channel=None,
|
||||
core_network_mode=None,
|
||||
):
|
||||
core_ids = device_plane_service_container_ids("device-control-core")
|
||||
manager_ids = device_plane_service_container_ids("device-manager")
|
||||
if len(core_ids) != 1 or len(manager_ids) != 1:
|
||||
@@ -13824,12 +14142,22 @@ def validate_device_manager_control_plane_runtime(*, require_edge_channel=None):
|
||||
core_networks = set(
|
||||
((core.get("NetworkSettings") or {}).get("Networks") or {}).keys()
|
||||
)
|
||||
expected_core_networks = {
|
||||
DEVICE_PLANE_PRIVATE_NETWORK,
|
||||
DEVICE_PLANE_CONTROL_NETWORK,
|
||||
}
|
||||
if require_edge_channel:
|
||||
expected_core_networks.add(DEVICE_PLANE_EGRESS_NETWORK)
|
||||
if core_network_mode not in (None, "private-egress"):
|
||||
die("Device Control Core Edge channel network mode is invalid")
|
||||
if core_network_mode == "private-egress" and not require_edge_channel:
|
||||
die("Device Control Core private egress requires Edge channel")
|
||||
if core_network_mode == "private-egress":
|
||||
expected_core_networks = {
|
||||
DEVICE_PLANE_PRIVATE_NETWORK,
|
||||
DEVICE_PLANE_EGRESS_NETWORK,
|
||||
}
|
||||
else:
|
||||
expected_core_networks = {
|
||||
DEVICE_PLANE_PRIVATE_NETWORK,
|
||||
DEVICE_PLANE_CONTROL_NETWORK,
|
||||
}
|
||||
if require_edge_channel:
|
||||
expected_core_networks.add(DEVICE_PLANE_EGRESS_NETWORK)
|
||||
if core_networks != expected_core_networks:
|
||||
die("Device Control Core Edge channel network boundary mismatch")
|
||||
manager_mounts = {
|
||||
@@ -15883,6 +16211,9 @@ def component_compose_files(
|
||||
edge_upgrade_v2_descriptor = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL
|
||||
)
|
||||
edge_upgrade_v4_descriptor = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
||||
)
|
||||
edge_override = (
|
||||
DEVICE_PLANE_ROOT / DEVICE_PLANE_EDGE_CORE_CHANNEL_COMPOSE_REL
|
||||
)
|
||||
@@ -15897,6 +16228,10 @@ def component_compose_files(
|
||||
edge_upgrade_v2_descriptor.exists()
|
||||
or edge_upgrade_v2_descriptor.is_symlink()
|
||||
)
|
||||
edge_upgrade_v4_descriptor_exists = (
|
||||
edge_upgrade_v4_descriptor.exists()
|
||||
or edge_upgrade_v4_descriptor.is_symlink()
|
||||
)
|
||||
edge_override_exists = edge_override.exists() or edge_override.is_symlink()
|
||||
if (
|
||||
edge_override_exists
|
||||
@@ -15904,6 +16239,7 @@ def component_compose_files(
|
||||
edge_descriptor_exists
|
||||
or edge_upgrade_descriptor_exists
|
||||
or edge_upgrade_v2_descriptor_exists
|
||||
or edge_upgrade_v4_descriptor_exists
|
||||
)
|
||||
or edge_upgrade_descriptor_exists
|
||||
and not edge_descriptor_exists
|
||||
@@ -15911,6 +16247,12 @@ def component_compose_files(
|
||||
and not (
|
||||
edge_descriptor_exists and edge_upgrade_descriptor_exists
|
||||
)
|
||||
or edge_upgrade_v4_descriptor_exists
|
||||
and not (
|
||||
edge_descriptor_exists
|
||||
and edge_upgrade_descriptor_exists
|
||||
and edge_upgrade_v2_descriptor_exists
|
||||
)
|
||||
):
|
||||
die("installed Device Edge Core channel source is incomplete")
|
||||
if edge_descriptor_exists:
|
||||
@@ -15988,6 +16330,35 @@ def component_compose_files(
|
||||
)
|
||||
):
|
||||
die("installed Device Edge Core channel upgrade v2 descriptor drift detected")
|
||||
if edge_upgrade_v4_descriptor_exists:
|
||||
if (
|
||||
upgrade_v2_id
|
||||
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID
|
||||
or edge_upgrade_v4_descriptor.is_symlink()
|
||||
or not edge_upgrade_v4_descriptor.is_file()
|
||||
or sha256_file(
|
||||
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml"
|
||||
)
|
||||
!= DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_BASE_COMPOSE_SHA256
|
||||
):
|
||||
die("installed Device Edge Core channel upgrade v4 predecessor drift detected")
|
||||
upgrade_v4 = read_strict_json(
|
||||
edge_upgrade_v4_descriptor,
|
||||
"installed Device Edge Core channel upgrade v4 descriptor",
|
||||
max_bytes=16 * 1024,
|
||||
)
|
||||
upgrade_v4_id = upgrade_v4.get("transitionId")
|
||||
if (
|
||||
not isinstance(upgrade_v4_id, str)
|
||||
or not re.fullmatch(
|
||||
r"[A-Za-z0-9._-]{1,96}", upgrade_v4_id
|
||||
)
|
||||
or upgrade_v4
|
||||
!= expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
upgrade_v4_id
|
||||
)
|
||||
):
|
||||
die("installed Device Edge Core channel upgrade v4 descriptor drift detected")
|
||||
files = (*files, edge_override)
|
||||
overlay = DEVICE_PLANE_ROOT / DEVICE_PLANE_BACKHAUL_TARGET_COMPOSE_REL
|
||||
if overlay.exists() or overlay.is_symlink():
|
||||
@@ -19983,6 +20354,19 @@ def plan_artifact(artifact):
|
||||
f"{predecessor['artifactSha256']}"
|
||||
)
|
||||
print("device_edge_channel_endpoint_policy=public-ipv4-standard-https-tcp-443-only")
|
||||
if "coreNetworks" in edge_descriptor:
|
||||
print(
|
||||
"device_edge_channel_core_networks="
|
||||
f"{','.join(edge_descriptor['coreNetworks'])}"
|
||||
)
|
||||
print(
|
||||
"device_edge_channel_removed_core_network="
|
||||
f"{edge_descriptor['removedCoreNetwork']}"
|
||||
)
|
||||
print(
|
||||
"device_edge_channel_compose_compatibility="
|
||||
f"{edge_descriptor['composeCompatibility']}"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
"device_plane_predecessor_patch="
|
||||
@@ -22600,9 +22984,18 @@ def run_healthchecks(component, entries=None, services=None):
|
||||
)
|
||||
for check in component_healthchecks(component, entries, services):
|
||||
healthcheck_url(check)
|
||||
validate_device_manager_control_plane_runtime(
|
||||
require_edge_channel=True
|
||||
)
|
||||
if is_device_plane_edge_core_channel_upgrade_v4_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(
|
||||
require_edge_channel=True,
|
||||
)
|
||||
return
|
||||
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
|
||||
return
|
||||
|
||||
@@ -364,6 +364,97 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_edge_core_channel_upgrade_v4_is_core_only_and_pins_upgrade_021(self):
|
||||
patch_id = "device-edge-core-channel-upgrade-v4-unit-001"
|
||||
manifest, entries, names, result = self.assert_deterministic_artifact(
|
||||
"build-device-edge-core-channel-bootstrap-artifact.mjs",
|
||||
patch_id,
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
||||
)
|
||||
self.assertEqual(manifest["component"], "device-plane")
|
||||
self.assertEqual(
|
||||
RUNNER.component_services("device-plane", entries),
|
||||
("device-control-core",),
|
||||
)
|
||||
self.assertEqual(len(RUNNER.component_builds("device-plane", entries)), 1)
|
||||
self.assertEqual(result["services"], ["device-control-core"])
|
||||
self.assertIn(
|
||||
"payload/deployment/device-edge-core-channel-upgrade-v4.json",
|
||||
names,
|
||||
)
|
||||
self.assertIn(
|
||||
"payload/docker-compose.device-plane.yml",
|
||||
names,
|
||||
)
|
||||
descriptor = (
|
||||
RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
patch_id
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["upgradePredecessor"]["patchId"],
|
||||
"device-edge-core-channel-upgrade-v2-20260812-021",
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["upgradePredecessor"]["artifactSha256"],
|
||||
"e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867",
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["failedAttempt"]["patchId"],
|
||||
"device-edge-core-channel-upgrade-v3-20260812-022",
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["failedAttempt"]["artifactSha256"],
|
||||
"9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613",
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["coreNetworks"],
|
||||
["device-plane-private", "device-plane-egress"],
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["removedCoreNetwork"],
|
||||
"device-plane-control",
|
||||
)
|
||||
self.assertEqual(
|
||||
descriptor["composeCompatibility"],
|
||||
"synology-compose-v2.20-no-gw-priority",
|
||||
)
|
||||
self.assertTrue(
|
||||
RUNNER.is_device_plane_edge_core_channel_upgrade_v4_slice(
|
||||
"device-plane",
|
||||
entries,
|
||||
)
|
||||
)
|
||||
|
||||
def test_edge_core_channel_upgrade_v4_rejects_installed_marker(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-edge-upgrade-v4-installed-",
|
||||
) as directory:
|
||||
root = Path(directory)
|
||||
marker = root / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL
|
||||
marker.parent.mkdir(parents=True)
|
||||
marker.write_text("{}\n", encoding="utf-8")
|
||||
descriptor = (
|
||||
RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
"device-edge-core-channel-upgrade-v4-unit-002"
|
||||
)
|
||||
)
|
||||
with (
|
||||
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_edge_core_channel_upgrade_v4_payload",
|
||||
return_value=descriptor,
|
||||
),
|
||||
):
|
||||
with self.assertRaisesRegex(
|
||||
RUNNER.DeployError,
|
||||
"upgrade v4 is already installed",
|
||||
):
|
||||
RUNNER.validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
||||
root / "payload"
|
||||
)
|
||||
|
||||
def test_edge_core_channel_upgrade_v2_rejects_installed_marker(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-edge-upgrade-v2-installed-",
|
||||
@@ -508,6 +599,162 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_edge_core_channel_upgrade_v4_accepts_exact_applied_021(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-edge-upgrade-v4-predecessor-",
|
||||
) as directory:
|
||||
root = Path(directory)
|
||||
applied = root / "applied"
|
||||
failed = root / "failed"
|
||||
backups = root / "backups"
|
||||
temporary = root / "tmp"
|
||||
device_plane = root / "device-plane"
|
||||
for path in (
|
||||
applied,
|
||||
failed,
|
||||
backups,
|
||||
temporary,
|
||||
device_plane / "deployment",
|
||||
):
|
||||
path.mkdir(parents=True)
|
||||
predecessor_patch = (
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID
|
||||
)
|
||||
predecessor_sha = (
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256
|
||||
)
|
||||
artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz"
|
||||
artifact = applied / artifact_name
|
||||
artifact.write_bytes(b"reviewed-upgrade-021")
|
||||
installed_base = device_plane / "docker-compose.device-plane.yml"
|
||||
installed_base.write_text("services: {}\n", encoding="utf-8")
|
||||
state_file = root / "applied.jsonl"
|
||||
state_file.write_text(
|
||||
json.dumps({
|
||||
"id": predecessor_patch,
|
||||
"artifact": artifact_name,
|
||||
"component": "device-plane",
|
||||
"sha256": predecessor_sha,
|
||||
"status": "ok",
|
||||
}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
failed_artifact = (
|
||||
failed
|
||||
/ RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT
|
||||
)
|
||||
failed_artifact.write_bytes(b"reviewed-failed-upgrade-022")
|
||||
failed_backup = (
|
||||
backups
|
||||
/ RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID
|
||||
)
|
||||
failed_backup.mkdir()
|
||||
failed_state_file = root / "failed.jsonl"
|
||||
failed_state_file.write_text(
|
||||
json.dumps({
|
||||
"id": RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID,
|
||||
"artifact": failed_artifact.name,
|
||||
"backup_id": failed_backup.name,
|
||||
"component": "device-plane",
|
||||
"sha256": RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256,
|
||||
"status": "failed",
|
||||
"started_apply": True,
|
||||
"rollback_status": "ok:device-plane-overlay:source+runtime-restored:8",
|
||||
"message": "Device Control Core Edge channel network boundary mismatch",
|
||||
}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
descriptor = (
|
||||
RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor(
|
||||
"device-edge-core-channel-upgrade-v4-unit-003"
|
||||
)
|
||||
)
|
||||
expected_source = {"source": "upgrade-021"}
|
||||
with (
|
||||
mock.patch.object(RUNNER, "APPLIED_DIR", applied),
|
||||
mock.patch.object(RUNNER, "FAILED_DIR", failed),
|
||||
mock.patch.object(RUNNER, "BACKUPS_DIR", backups),
|
||||
mock.patch.object(RUNNER, "TMP_DIR", temporary),
|
||||
mock.patch.object(RUNNER, "STATE_FILE", state_file),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"FAILED_STATE_FILE",
|
||||
failed_state_file,
|
||||
),
|
||||
mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_edge_core_channel_upgrade_v4_payload",
|
||||
return_value=descriptor,
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"sha256_file",
|
||||
side_effect=lambda path: (
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256
|
||||
if Path(path) == installed_base
|
||||
else RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256
|
||||
if Path(path) == failed_artifact
|
||||
else predecessor_sha
|
||||
),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"load_artifact",
|
||||
return_value=(
|
||||
{
|
||||
"id": predecessor_patch,
|
||||
"component": "device-plane",
|
||||
"type": "app-overlay",
|
||||
},
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES,
|
||||
root / "predecessor-payload",
|
||||
),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_plane_edge_core_channel_upgrade_v2_payload",
|
||||
) as validate_predecessor,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"collect_exact_files",
|
||||
side_effect=(expected_source, expected_source),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"inspect_device_edge_channel_core_identity_state",
|
||||
return_value="valid-reuse-at-apply",
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"healthcheck_compose_service",
|
||||
) as health,
|
||||
):
|
||||
result = (
|
||||
RUNNER.validate_device_plane_edge_core_channel_upgrade_v4_predecessor(
|
||||
root / "payload"
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
result["mode"],
|
||||
"edge-core-channel-private-plus-egress-upgrade-v4",
|
||||
)
|
||||
self.assertEqual(result["upgradeArtifact"], artifact)
|
||||
validate_predecessor.assert_called_once_with(
|
||||
root / "predecessor-payload",
|
||||
expected_transition_id=predecessor_patch,
|
||||
)
|
||||
self.assertEqual(
|
||||
[call.args for call in health.call_args_list],
|
||||
[
|
||||
("device-plane", "device-control-core"),
|
||||
("device-plane", "device-manager"),
|
||||
("device-plane", "device-gateway"),
|
||||
("device-plane", "device-postgres"),
|
||||
],
|
||||
)
|
||||
|
||||
def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self):
|
||||
predecessor = device_manager_release_v1_descriptor(
|
||||
release_id="device-manager-release-20260811-010",
|
||||
@@ -747,6 +994,24 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
allow_invalid_unexported_recovery=False
|
||||
)
|
||||
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"ensure_platform_runtime_secret",
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"ensure_device_edge_channel_core_identity",
|
||||
) as ensure_edge_identity,
|
||||
):
|
||||
RUNNER.prepare_component_runtime(
|
||||
"device-plane",
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES,
|
||||
)
|
||||
ensure_edge_identity.assert_called_once_with(
|
||||
allow_invalid_unexported_recovery=False
|
||||
)
|
||||
|
||||
def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self):
|
||||
entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES
|
||||
services = ("device-control-core", "device-manager")
|
||||
@@ -811,7 +1076,46 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
services,
|
||||
)[0]
|
||||
)
|
||||
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
|
||||
runtime_acceptance.assert_called_once_with(
|
||||
require_edge_channel=True
|
||||
)
|
||||
|
||||
def test_upgrade_v4_apply_gate_checks_preserved_runtime_and_edge_contract(self):
|
||||
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
||||
services = ("device-control-core",)
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"healthcheck_compose_service_with_grace",
|
||||
) as service_health,
|
||||
mock.patch.object(RUNNER, "healthcheck_url") as url_health,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_manager_control_plane_runtime",
|
||||
) as runtime_acceptance,
|
||||
):
|
||||
RUNNER.run_healthchecks("device-plane", entries, services)
|
||||
|
||||
self.assertEqual(
|
||||
[call.args for call in service_health.call_args_list],
|
||||
[
|
||||
("device-plane", "device-control-core"),
|
||||
("device-plane", "device-manager"),
|
||||
("device-plane", "device-gateway"),
|
||||
("device-plane", "device-postgres"),
|
||||
],
|
||||
)
|
||||
url_health.assert_called_once_with(
|
||||
RUNNER.component_healthchecks(
|
||||
"device-plane",
|
||||
entries,
|
||||
services,
|
||||
)[0]
|
||||
)
|
||||
runtime_acceptance.assert_called_once_with(
|
||||
require_edge_channel=True,
|
||||
core_network_mode="private-egress",
|
||||
)
|
||||
|
||||
def test_activation_resolves_predecessor_from_descriptor_and_journal(self):
|
||||
with tempfile.TemporaryDirectory(
|
||||
@@ -1274,6 +1578,79 @@ class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase):
|
||||
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
|
||||
self.assertEqual(result, f"source+runtime-restored:{len(entries)}")
|
||||
|
||||
def test_edge_upgrade_v4_rollback_restores_upgrade_021_core_runtime(self):
|
||||
entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES
|
||||
missing = {
|
||||
RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL,
|
||||
}
|
||||
existing = [entry for entry in entries if entry not in missing]
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="nodedc-device-edge-upgrade-v4-rollback-",
|
||||
) as directory:
|
||||
backup = Path(directory) / "backup"
|
||||
backup.mkdir()
|
||||
(backup / "existing-files.txt").write_text(
|
||||
"\n".join(existing) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(backup / "missing-files.txt").write_text(
|
||||
"\n".join(sorted(missing)) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(backup / "runtime-before.json").write_text(
|
||||
json.dumps(healthy_device_plane_inventory(include_manager=True)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"stop_and_remove_compose_services",
|
||||
) as stop,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"restore_platform_overlay",
|
||||
return_value=len(entries),
|
||||
),
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"run_component_runtime",
|
||||
) as restore_runtime,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"healthcheck_compose_service_with_grace",
|
||||
) as restore_health,
|
||||
mock.patch.object(
|
||||
RUNNER,
|
||||
"validate_device_manager_control_plane_runtime",
|
||||
) as runtime_acceptance,
|
||||
):
|
||||
result = RUNNER.rollback_device_plane_apply(
|
||||
Path(directory) / "live",
|
||||
backup,
|
||||
entries,
|
||||
"test-stamp",
|
||||
True,
|
||||
("device-control-core",),
|
||||
)
|
||||
|
||||
stop.assert_not_called()
|
||||
restore_runtime.assert_called_once_with(
|
||||
"device-plane",
|
||||
existing,
|
||||
("device-control-core",),
|
||||
)
|
||||
self.assertEqual(
|
||||
[call.args for call in restore_health.call_args_list],
|
||||
[
|
||||
("device-plane", "device-control-core"),
|
||||
("device-plane", "device-manager"),
|
||||
("device-plane", "device-gateway"),
|
||||
("device-plane", "device-postgres"),
|
||||
],
|
||||
)
|
||||
runtime_acceptance.assert_called_once_with(require_edge_channel=True)
|
||||
self.assertEqual(result, f"source+runtime-restored:{len(entries)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
Reference in New Issue
Block a user