feat(device-plane): add fail-closed deploy foundation
This commit is contained in:
@@ -37,6 +37,21 @@ MAP_GATEWAY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-gateway-admin-secret"
|
||||
MAP_EGRESS_PROXY_SECRET_FILE = MAP_GATEWAY_SECRET_DIR / "map-egress-proxy-token"
|
||||
PROXY_CONTUR_ENV_FILE = Path("/volume1/docker/proxy-contur/.env")
|
||||
DC_AMD_PROXY_RUNTIME_DIR = Path("/volume1/docker/dc-amd-proxy/runtime")
|
||||
DEVICE_PLANE_ROOT = Path("/volume1/docker/nodedc-device-plane")
|
||||
DEVICE_PLANE_SECRET_DIR = DEVICE_PLANE_ROOT / "secrets"
|
||||
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_IDENTIFIER_PEPPER_FILE = DEVICE_PLANE_SECRET_DIR / "identifier-pepper"
|
||||
DEVICE_PLANE_CONTROL_CORE_IMAGE = "nodedc/device-control-core:local"
|
||||
DEVICE_PLANE_GATEWAY_IMAGE = "nodedc/device-gateway:local"
|
||||
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL = (
|
||||
"deployment/device-postgres-bootstrap-v1.json"
|
||||
)
|
||||
DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES = (
|
||||
"docker-compose.device-plane.yml",
|
||||
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
||||
)
|
||||
DEVICE_PLANE_POSTGRES_VOLUME = "nodedc-device-plane-postgres-data"
|
||||
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR = MAP_GATEWAY_SECRET_DIR / "external-data-plane-provisioner"
|
||||
EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "token"
|
||||
ENGINE_CREDENTIAL_PROVISIONER_PRIVATE_KEY_FILE = EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_DIR / "engine-credential-provisioner-ed25519.pem"
|
||||
@@ -1140,6 +1155,19 @@ COMPONENTS = {
|
||||
"http://172.22.0.222:9920/healthz",
|
||||
),
|
||||
},
|
||||
"device-plane": {
|
||||
"payload_root": DEVICE_PLANE_ROOT,
|
||||
"compose_root": DEVICE_PLANE_ROOT,
|
||||
"compose_project": "nodedc-device-plane",
|
||||
"compose_files": (
|
||||
DEVICE_PLANE_ROOT / "docker-compose.device-plane.yml",
|
||||
),
|
||||
"bootstrap_root": True,
|
||||
"compose_no_deps": True,
|
||||
# PostgreSQL is durable state infrastructure. Normal application
|
||||
# overlays can rebuild/recreate only these two stateless services.
|
||||
"services": ("device-control-core", "device-gateway"),
|
||||
},
|
||||
"proxy-contur": {
|
||||
"payload_root": Path("/volume1/docker/proxy-contur"),
|
||||
"compose_root": Path("/volume1/docker/proxy-contur"),
|
||||
@@ -2297,6 +2325,12 @@ def denied_payload_path(component, rel):
|
||||
"infra/docker-compose.module-foundry.yml",
|
||||
):
|
||||
pass
|
||||
elif component == "device-plane" and rel in (
|
||||
"docker-compose.device-plane.yml",
|
||||
"services/device-control-core/Dockerfile",
|
||||
"services/device-gateway/Dockerfile",
|
||||
):
|
||||
pass
|
||||
elif component == "proxy-contur" and rel in (
|
||||
"Dockerfile",
|
||||
"docker-compose.yml",
|
||||
@@ -2426,6 +2460,25 @@ def denied_payload_path(component, rel):
|
||||
return "module-foundry env file"
|
||||
if rel.startswith(("Dockerfile.bak", "infra/docker-compose.module-foundry.yml.bak")):
|
||||
return "module-foundry backup file"
|
||||
elif component == "device-plane":
|
||||
runtime_prefixes = (
|
||||
"runtime",
|
||||
"secrets",
|
||||
"data",
|
||||
"logs",
|
||||
"backups",
|
||||
"node_modules",
|
||||
)
|
||||
if rel == ".env" or rel.startswith(".env."):
|
||||
return "device-plane env file"
|
||||
if "test" in parts:
|
||||
return "device-plane test path"
|
||||
if rel.startswith((
|
||||
"docker-compose.device-plane.yml.bak",
|
||||
"services/device-control-core/Dockerfile.bak",
|
||||
"services/device-gateway/Dockerfile.bak",
|
||||
)):
|
||||
return "device-plane backup file"
|
||||
elif component == "n8n-private-extension":
|
||||
# An extension release is inert data at this boundary. Activation is
|
||||
# Engine-owned and must never be smuggled into the artifact as a script,
|
||||
@@ -2676,6 +2729,27 @@ def allowed_payload_path(component, rel):
|
||||
)):
|
||||
return True
|
||||
|
||||
if component == "device-plane":
|
||||
if rel in (
|
||||
".dockerignore",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"docker-compose.device-plane.yml",
|
||||
DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
||||
"packages/device-protocol-contract",
|
||||
"packages/arusnavi-b2-adapter",
|
||||
"services/device-control-core",
|
||||
"services/device-gateway",
|
||||
):
|
||||
return True
|
||||
if rel.startswith((
|
||||
"packages/device-protocol-contract/",
|
||||
"packages/arusnavi-b2-adapter/",
|
||||
"services/device-control-core/",
|
||||
"services/device-gateway/",
|
||||
)):
|
||||
return True
|
||||
|
||||
if component == "n8n-private-extension":
|
||||
parts = PurePosixPath(rel).parts
|
||||
if (
|
||||
@@ -7681,6 +7755,11 @@ def load_artifact(artifact, work_dir):
|
||||
die(f"files.txt entry missing in payload: {rel}")
|
||||
|
||||
validate_payload_tree(manifest["component"], payload_dir, entries)
|
||||
if is_device_plane_postgres_bootstrap_slice(
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
validate_device_plane_postgres_bootstrap_payload(payload_dir)
|
||||
if manifest["component"] == "n8n-private-extension":
|
||||
validate_n8n_private_extension_release(payload_dir, entries)
|
||||
if manifest["component"] == "engine":
|
||||
@@ -7972,6 +8051,79 @@ def component_root(component):
|
||||
return COMPONENTS[component]["payload_root"]
|
||||
|
||||
|
||||
def is_device_plane_postgres_bootstrap_slice(component, entries):
|
||||
return (
|
||||
component == "device-plane"
|
||||
and entries is not None
|
||||
and tuple(entries) == DEVICE_PLANE_POSTGRES_BOOTSTRAP_ENTRIES
|
||||
)
|
||||
|
||||
|
||||
def validate_device_plane_postgres_bootstrap_payload(payload_dir):
|
||||
descriptor = read_strict_json(
|
||||
payload_dir / DEVICE_PLANE_POSTGRES_BOOTSTRAP_REL,
|
||||
"Device Plane PostgreSQL bootstrap descriptor",
|
||||
max_bytes=8 * 1024,
|
||||
)
|
||||
expected = {
|
||||
"schemaVersion": "nodedc.device-plane.postgres-bootstrap.v1",
|
||||
"service": "device-postgres",
|
||||
"volume": DEVICE_PLANE_POSTGRES_VOLUME,
|
||||
"mode": "create-if-absent",
|
||||
"ordinaryApplicationSelection": "forbidden",
|
||||
"rollbackVolumePolicy": "preserve",
|
||||
}
|
||||
if descriptor != expected:
|
||||
die("Device Plane PostgreSQL bootstrap descriptor mismatch")
|
||||
return descriptor
|
||||
|
||||
|
||||
def preflight_device_plane_postgres_bootstrap():
|
||||
container_result = subprocess.run(
|
||||
[
|
||||
str(DOCKER),
|
||||
"container",
|
||||
"ls",
|
||||
"-a",
|
||||
"--filter",
|
||||
"label=com.docker.compose.project=nodedc-device-plane",
|
||||
"--filter",
|
||||
"label=com.docker.compose.service=device-postgres",
|
||||
"--format",
|
||||
"{{.ID}}",
|
||||
],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if container_result.returncode != 0:
|
||||
die("Device Plane PostgreSQL container preflight failed")
|
||||
container_ids = [
|
||||
line.strip()
|
||||
for line in container_result.stdout.splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
if container_ids:
|
||||
die("Device Plane PostgreSQL container already exists")
|
||||
|
||||
volume_result = subprocess.run(
|
||||
[
|
||||
str(DOCKER),
|
||||
"volume",
|
||||
"inspect",
|
||||
DEVICE_PLANE_POSTGRES_VOLUME,
|
||||
],
|
||||
check=False,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
if volume_result.returncode == 0:
|
||||
die("Device Plane PostgreSQL volume already exists")
|
||||
if volume_result.returncode != 1:
|
||||
die("Device Plane PostgreSQL volume preflight failed")
|
||||
return "absent"
|
||||
|
||||
|
||||
def component_compose_root(component):
|
||||
return COMPONENTS[component].get("compose_root", component_root(component))
|
||||
|
||||
@@ -9491,6 +9643,13 @@ def is_platform_provider_catalog_only(entries):
|
||||
|
||||
|
||||
def component_services(component, entries=None):
|
||||
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
||||
# This exact one-time transition is the only Device Plane artifact that
|
||||
# may select durable state. Its preflight requires both container and
|
||||
# named volume to be absent, so --force-recreate cannot touch an
|
||||
# installed database.
|
||||
return ("device-postgres",)
|
||||
|
||||
if is_engine_l2_closed_loop_slice(component, entries):
|
||||
# The failed 030 apply published both the backend source and the built
|
||||
# UI before Compose rejected the descriptor/source mismatch. The exact
|
||||
@@ -9549,6 +9708,45 @@ def component_services(component, entries=None):
|
||||
# authorization normalization inside the already-active backend.
|
||||
return ("nodedc-backend",)
|
||||
|
||||
if component == "device-plane" and entries is not None:
|
||||
selected = []
|
||||
|
||||
def add(*services):
|
||||
for service in services:
|
||||
if service not in selected:
|
||||
selected.append(service)
|
||||
|
||||
touches_common = any(
|
||||
rel in (
|
||||
".dockerignore",
|
||||
"package.json",
|
||||
"package-lock.json",
|
||||
"docker-compose.device-plane.yml",
|
||||
"packages/device-protocol-contract",
|
||||
"packages/arusnavi-b2-adapter",
|
||||
)
|
||||
or rel.startswith((
|
||||
"packages/device-protocol-contract/",
|
||||
"packages/arusnavi-b2-adapter/",
|
||||
))
|
||||
for rel in entries
|
||||
)
|
||||
touches_core = any(
|
||||
rel == "services/device-control-core"
|
||||
or rel.startswith("services/device-control-core/")
|
||||
for rel in entries
|
||||
)
|
||||
touches_gateway = any(
|
||||
rel == "services/device-gateway"
|
||||
or rel.startswith("services/device-gateway/")
|
||||
for rel in entries
|
||||
)
|
||||
if touches_common or touches_core:
|
||||
add("device-control-core")
|
||||
if touches_common or touches_gateway:
|
||||
add("device-gateway")
|
||||
return tuple(selected)
|
||||
|
||||
if component == "dc-cms" and entries is not None:
|
||||
selected = []
|
||||
|
||||
@@ -9783,6 +9981,42 @@ def component_build_args(component, entries=None):
|
||||
|
||||
|
||||
def component_builds(component, entries=None):
|
||||
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
||||
return ()
|
||||
|
||||
if component == "device-plane" and entries is not None:
|
||||
selected_services = component_services(component, entries)
|
||||
builds = []
|
||||
if "device-control-core" in selected_services:
|
||||
builds.append((
|
||||
DEVICE_PLANE_ROOT,
|
||||
(
|
||||
"build",
|
||||
"--no-cache",
|
||||
"--network=host",
|
||||
"-f",
|
||||
"services/device-control-core/Dockerfile",
|
||||
"-t",
|
||||
DEVICE_PLANE_CONTROL_CORE_IMAGE,
|
||||
".",
|
||||
),
|
||||
))
|
||||
if "device-gateway" in selected_services:
|
||||
builds.append((
|
||||
DEVICE_PLANE_ROOT,
|
||||
(
|
||||
"build",
|
||||
"--no-cache",
|
||||
"--network=host",
|
||||
"-f",
|
||||
"services/device-gateway/Dockerfile",
|
||||
"-t",
|
||||
DEVICE_PLANE_GATEWAY_IMAGE,
|
||||
".",
|
||||
),
|
||||
))
|
||||
return tuple(builds)
|
||||
|
||||
if component == "platform" and entries is not None:
|
||||
touches_compose = any(rel == "platform/docker-compose.platform-http.yml" for rel in entries)
|
||||
touches_notification = any(rel == "platform/notification-core" or rel.startswith("platform/notification-core/") for rel in entries)
|
||||
@@ -11739,6 +11973,7 @@ def plan_artifact(artifact):
|
||||
mcp_registered_execution_profiles_preflight = None
|
||||
mcp_gelios_units_items_preflight = None
|
||||
provider_catalog_preflight = None
|
||||
device_plane_postgres_preflight = None
|
||||
with tempfile.TemporaryDirectory(prefix="plan-", dir=TMP_DIR) as tmp:
|
||||
manifest, entries, payload_dir = load_artifact(artifact, Path(tmp))
|
||||
reject_terminal_engine_l2_failed_artifact(manifest, sha)
|
||||
@@ -11875,6 +12110,13 @@ def plan_artifact(artifact):
|
||||
)
|
||||
if is_engine_provider_security_catalog_slice(manifest["component"], entries):
|
||||
provider_catalog_preflight = preflight_engine_provider_security_catalog_predecessor()
|
||||
if is_device_plane_postgres_bootstrap_slice(
|
||||
manifest["component"],
|
||||
entries,
|
||||
):
|
||||
device_plane_postgres_preflight = (
|
||||
preflight_device_plane_postgres_bootstrap()
|
||||
)
|
||||
|
||||
component = manifest["component"]
|
||||
root = component_root(component)
|
||||
@@ -13246,6 +13488,22 @@ def plan_artifact(artifact):
|
||||
print(f"runtime_grants=runner-managed:{FOUNDRY_BINDING_GRANTS_DIR}")
|
||||
print(f"runtime_private_key=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PRIVATE_KEY_FILE}")
|
||||
print(f"runtime_public_trust=runner-managed:{FOUNDRY_EDP_MANAGED_PROVISIONER_PUBLIC_KEY_FILE}")
|
||||
if component == "device-plane":
|
||||
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_POSTGRES_PASSWORD_FILE}")
|
||||
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE}")
|
||||
print(f"runtime_secret=runner-managed:{DEVICE_PLANE_IDENTIFIER_PEPPER_FILE}")
|
||||
print("device_postgres=preserved-prerequisite:not-selected")
|
||||
print("device_postgres_volume=preserved:nodedc-device-plane-postgres-data")
|
||||
print("device_gateway_public_ingress=disabled")
|
||||
print("device_gateway_command_transport=disabled")
|
||||
print("gelios=untouched")
|
||||
if device_plane_postgres_preflight is not None:
|
||||
print(
|
||||
"device_postgres_bootstrap="
|
||||
f"{device_plane_postgres_preflight}"
|
||||
)
|
||||
print("device_postgres_bootstrap_mode=create-if-absent")
|
||||
print("device_postgres_rollback_volume=preserve")
|
||||
if touches_external_data_plane:
|
||||
print(f"runtime_secret=runner-managed:{EXTERNAL_DATA_PLANE_PROVISIONER_SECRET_FILE}")
|
||||
print(f"runtime_grants=runner-managed:{EXTERNAL_DATA_PLANE_READER_GRANTS_DIR}")
|
||||
@@ -13527,6 +13785,71 @@ def rollback_platform_apply(root, backup_dir, entries, current_stamp, runtime_st
|
||||
return f"source+runtime-restored:{restored_count}"
|
||||
|
||||
|
||||
def rollback_device_plane_apply(
|
||||
root,
|
||||
backup_dir,
|
||||
entries,
|
||||
current_stamp,
|
||||
runtime_started,
|
||||
applied_services,
|
||||
):
|
||||
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
||||
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
||||
existing_set, _missing_set = validate_backup_partition(
|
||||
entries,
|
||||
existing,
|
||||
missing,
|
||||
"Device Plane runtime rollback",
|
||||
)
|
||||
baseline_entries = [rel for rel in entries if rel in existing_set]
|
||||
compose_was_installed = "docker-compose.device-plane.yml" in existing_set
|
||||
baseline_services = (
|
||||
component_services("device-plane", baseline_entries)
|
||||
if compose_was_installed
|
||||
else ()
|
||||
)
|
||||
candidate_only_services = tuple(
|
||||
service
|
||||
for service in applied_services
|
||||
if service not in baseline_services
|
||||
)
|
||||
candidate_cleanup_failed = False
|
||||
if runtime_started and candidate_only_services:
|
||||
# Remove only candidate services while the candidate Compose file is
|
||||
# still present. PostgreSQL can appear only in the exact one-time
|
||||
# bootstrap slice; volume flags are deliberately never used.
|
||||
try:
|
||||
stop_and_remove_compose_services(
|
||||
"device-plane",
|
||||
candidate_only_services,
|
||||
)
|
||||
except Exception:
|
||||
candidate_cleanup_failed = True
|
||||
|
||||
restored_count = restore_platform_overlay(
|
||||
root,
|
||||
backup_dir,
|
||||
entries,
|
||||
current_stamp,
|
||||
)
|
||||
if candidate_cleanup_failed:
|
||||
die("Device Plane candidate-only runtime cleanup failed after source restore")
|
||||
if not runtime_started or not baseline_services:
|
||||
return f"source-restored-runtime-unchanged:{restored_count}"
|
||||
|
||||
run_component_runtime(
|
||||
"device-plane",
|
||||
baseline_entries,
|
||||
baseline_services,
|
||||
)
|
||||
run_healthchecks(
|
||||
"device-plane",
|
||||
baseline_entries,
|
||||
baseline_services,
|
||||
)
|
||||
return f"source+runtime-restored:{restored_count}"
|
||||
|
||||
|
||||
def rollback_engine_apply(root, backup_dir, entries, current_stamp, runtime_started, applied_services):
|
||||
existing = read_backup_path_list(backup_dir / "existing-files.txt")
|
||||
missing = read_backup_path_list(backup_dir / "missing-files.txt")
|
||||
@@ -14130,6 +14453,24 @@ def prepare_component_runtime(component, entries=None):
|
||||
)
|
||||
return
|
||||
|
||||
if component == "device-plane":
|
||||
ensure_platform_runtime_secret(
|
||||
DEVICE_PLANE_POSTGRES_PASSWORD_FILE,
|
||||
MAP_GATEWAY_SECRET_RE,
|
||||
"device plane PostgreSQL",
|
||||
)
|
||||
ensure_platform_runtime_secret(
|
||||
DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE,
|
||||
MAP_GATEWAY_SECRET_RE,
|
||||
"device plane Gateway to Core",
|
||||
)
|
||||
ensure_platform_runtime_secret(
|
||||
DEVICE_PLANE_IDENTIFIER_PEPPER_FILE,
|
||||
MAP_GATEWAY_SECRET_RE,
|
||||
"device plane identifier pepper",
|
||||
)
|
||||
return
|
||||
|
||||
if component == "proxy-contur":
|
||||
sync_map_egress_proxy_secret()
|
||||
return
|
||||
@@ -14309,6 +14650,8 @@ def module_foundry_healthcheck():
|
||||
|
||||
|
||||
def component_healthchecks(component, entries=None, services=None):
|
||||
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
||||
return ()
|
||||
if is_engine_n8n_transition(component, entries):
|
||||
# The transition does not restart the Engine UI/backend generation.
|
||||
# Its own acceptance below verifies n8n readiness, image, mount, logs,
|
||||
@@ -14323,6 +14666,36 @@ def component_healthchecks(component, entries=None, services=None):
|
||||
return ()
|
||||
if component == "module-foundry":
|
||||
return (module_foundry_healthcheck(),)
|
||||
if component == "device-plane":
|
||||
selected_services = (
|
||||
tuple(services)
|
||||
if services is not None
|
||||
else component_services(component, entries)
|
||||
)
|
||||
checks = []
|
||||
if "device-control-core" in selected_services:
|
||||
checks.append({
|
||||
"url": "http://127.0.0.1:18120/healthz",
|
||||
"expected_json": {
|
||||
"ok": True,
|
||||
"service": "nodedc-device-control-core",
|
||||
"database": "ready",
|
||||
"discoveryIngest": "disabled",
|
||||
"commandTransport": "disabled",
|
||||
},
|
||||
})
|
||||
if "device-gateway" in selected_services:
|
||||
checks.append({
|
||||
"url": "http://127.0.0.1:18121/healthz",
|
||||
"expected_json": {
|
||||
"ok": True,
|
||||
"service": "nodedc-device-gateway",
|
||||
"tcpListener": "disabled",
|
||||
"publicIngress": "disabled",
|
||||
"commandTransport": "disabled",
|
||||
},
|
||||
})
|
||||
return tuple(checks)
|
||||
if (
|
||||
(
|
||||
is_engine_data_product_publish_grant_slice(component, entries)
|
||||
@@ -14712,6 +15085,11 @@ process.stdout.write('engine-l2-closed-loop:0.7.0:cas+safe-profile+external-plan
|
||||
|
||||
|
||||
def run_healthchecks(component, entries=None, services=None):
|
||||
if is_device_plane_postgres_bootstrap_slice(component, entries):
|
||||
if tuple(services or ()) != ("device-postgres",):
|
||||
die("Device Plane PostgreSQL bootstrap service set mismatch")
|
||||
healthcheck_compose_service("device-plane", "device-postgres")
|
||||
return
|
||||
if component == "platform" and entries is not None and is_platform_provider_catalog_only(entries):
|
||||
return
|
||||
if is_engine_l2_closed_loop_slice(component, entries):
|
||||
@@ -15504,6 +15882,11 @@ def apply_artifact(artifact):
|
||||
die(f"artifact sha already applied: {sha}")
|
||||
if state_has_patch_id(patch_id):
|
||||
die(f"patch id already applied: {patch_id}")
|
||||
if is_device_plane_postgres_bootstrap_slice(
|
||||
component,
|
||||
entries,
|
||||
):
|
||||
preflight_device_plane_postgres_bootstrap()
|
||||
if not root.is_dir():
|
||||
if bootstrap_root:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
@@ -16064,6 +16447,31 @@ def apply_artifact(artifact):
|
||||
except Exception as rollback_exc:
|
||||
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
||||
print("platform-automatic-rollback=failed", file=sys.stderr)
|
||||
elif (
|
||||
component == "device-plane"
|
||||
and entries is not None
|
||||
and services is not None
|
||||
):
|
||||
try:
|
||||
restored_state = rollback_device_plane_apply(
|
||||
root,
|
||||
backup_dir,
|
||||
entries,
|
||||
current_stamp,
|
||||
runtime_started,
|
||||
services,
|
||||
)
|
||||
rollback_status = f"ok:device-plane-overlay:{restored_state}"
|
||||
print(
|
||||
f"device-plane-automatic-rollback={rollback_status}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except Exception as rollback_exc:
|
||||
rollback_status = f"failed:{type(rollback_exc).__name__}"
|
||||
print(
|
||||
"device-plane-automatic-rollback=failed",
|
||||
file=sys.stderr,
|
||||
)
|
||||
failed_path = None
|
||||
if artifact.exists() and rollback_status != "deferred:reconciliation-required":
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user