136 lines
5.2 KiB
Python
136 lines
5.2 KiB
Python
"""V2R2C1 adapter using the existing V2R1C7 deployment primitives.
|
|
|
|
The service identity and loopback port remain stable for a safe cutover. The
|
|
new release is installed under its own immutable directory and keeps the
|
|
previous V2R1C7 release available for an explicit, owner-approved rollback.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from argparse import Namespace
|
|
from pathlib import Path
|
|
from typing import Any, Mapping
|
|
|
|
import v2r1c7_production_adapter as base
|
|
|
|
|
|
VERSION = "V2R2C1"
|
|
SERVICE = "aios-v2r1c7-production.service"
|
|
PORT = 18927
|
|
RELEASE_ROOT = Path("/opt/aios/releases")
|
|
CURRENT_LINK = Path("/opt/aios/v2r2c1")
|
|
ENV_FILE = Path("/etc/aios/aios-v2r2c1.env")
|
|
STATE_FILE = Path("/var/lib/aios/v2r2c1/deployment-state.json")
|
|
UNIT_FILE = Path("/etc/systemd/system/aios-v2r1c7-production.service")
|
|
ROLLBACK_ROOT = Path("/var/lib/aios/v2r2c1/rollback")
|
|
|
|
REQUIRED_MEMBERS = {
|
|
"payload/VERSION",
|
|
"payload/active-runtime/app/deployment_state.py",
|
|
"payload/active-runtime/app/v2r1c6_privacy_tools.py",
|
|
"payload/active-runtime/app/v2r2c1_health_service.py",
|
|
"payload/deploy/aios-v2/systemd/aios-v2r1c7-production.service",
|
|
"payload/deploy/aios-v2/v2r2c1-runtime-config.json",
|
|
"payload/deploy/aios-v2/v2r2c1-rollback.json",
|
|
"payload/scripts/probe_v2r2c1_health.py",
|
|
"payload/scripts/rollback_v2r2c1_release.sh",
|
|
"payload/BUILD-METADATA.json",
|
|
"payload/ARTIFACT-MANIFEST.json",
|
|
"payload/ROLLBACK-METADATA.json",
|
|
}
|
|
|
|
|
|
def _configure_base() -> None:
|
|
base.VERSION = VERSION
|
|
base.SERVICE = SERVICE
|
|
base.PORT = PORT
|
|
base.REQUIRED_MEMBERS = REQUIRED_MEMBERS
|
|
|
|
|
|
def _args() -> Namespace:
|
|
return Namespace(
|
|
release_root=RELEASE_ROOT,
|
|
current_link=CURRENT_LINK,
|
|
env_file=ENV_FILE,
|
|
state_file=STATE_FILE,
|
|
unit_file=UNIT_FILE,
|
|
health_timeout=45,
|
|
)
|
|
|
|
|
|
def load_request() -> dict[str, Any]:
|
|
_configure_base()
|
|
value = base.load_request()
|
|
if value.get("version") != VERSION or value.get("production_service") != SERVICE or int(value.get("production_port", -1)) != PORT:
|
|
raise ValueError("V2R2C1 production binding mismatch")
|
|
return value
|
|
|
|
|
|
def _backup(path: Path, name: str) -> None:
|
|
if path.is_file() and not path.is_symlink():
|
|
ROLLBACK_ROOT.mkdir(parents=True, exist_ok=True)
|
|
destination = ROLLBACK_ROOT / name
|
|
if not destination.exists():
|
|
shutil.copy2(path, destination)
|
|
|
|
|
|
def execute_request(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
_configure_base()
|
|
if value.get("schema") != "aios.production-executor-request.v1":
|
|
raise ValueError("request_schema_invalid")
|
|
if value.get("operation") == "apply":
|
|
_backup(UNIT_FILE, "aios-v2r1c7-production.service.previous")
|
|
_backup(Path("/etc/aios/aios-v2r1c7.env"), "aios-v2r1c7.env.previous")
|
|
_backup(Path("/var/lib/aios/v2r1c7/deployment-state.json"), "v2r1c7-deployment-state.previous")
|
|
return base.install_release(_args(), value)
|
|
if value.get("operation") == "probe":
|
|
return base.probe(_args(), value)
|
|
if value.get("operation") == "rollback":
|
|
return rollback(value)
|
|
raise ValueError("unsupported_operation")
|
|
|
|
|
|
def rollback(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
"""Explicit rollback only; this adapter never invokes it automatically."""
|
|
import subprocess
|
|
|
|
subprocess.run(["systemctl", "stop", SERVICE], check=False, capture_output=True, text=True, timeout=45)
|
|
previous_unit = ROLLBACK_ROOT / "aios-v2r1c7-production.service.previous"
|
|
previous_env = ROLLBACK_ROOT / "aios-v2r1c7.env.previous"
|
|
previous_state = ROLLBACK_ROOT / "v2r1c7-deployment-state.previous"
|
|
if not previous_unit.is_file() or not previous_env.is_file():
|
|
raise RuntimeError("ROLLBACK_RECEIPT_MISSING")
|
|
UNIT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
ENV_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(previous_unit, UNIT_FILE)
|
|
shutil.copy2(previous_env, Path("/etc/aios/aios-v2r1c7.env"))
|
|
if previous_state.is_file():
|
|
Path("/var/lib/aios/v2r1c7").mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(previous_state, Path("/var/lib/aios/v2r1c7/deployment-state.json"))
|
|
subprocess.run(["systemctl", "daemon-reload"], check=True, capture_output=True, text=True, timeout=45)
|
|
subprocess.run(["systemctl", "restart", SERVICE], check=True, capture_output=True, text=True, timeout=45)
|
|
result = base.health("http://127.0.0.1:18927/health", {
|
|
"version": "V2R1C7",
|
|
"target_oid": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("commit_oid")),
|
|
"artifact_sha256": str(json.loads(Path("/var/lib/aios/v2r1c7/deployment-state.json").read_text(encoding="utf-8")).get("artifact_sha256")),
|
|
})
|
|
return {"status": "ROLLED_BACK", "rollback_health": result, "automatic_rollback": False}
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
value = load_request()
|
|
result = execute_request(value)
|
|
print(json.dumps(result, sort_keys=True))
|
|
return 0
|
|
except Exception as exc:
|
|
print(json.dumps({"status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc)}, sort_keys=True))
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|