"""Cut over the verified dedicated console; preserve both previous deployments.""" from datetime import datetime, timezone import json from pathlib import Path import shutil import sqlite3 import subprocess import sys import time import urllib.request sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from deploy.dedicated import ROOT, NAME, IMAGE, UNIT, command, prepare, web_arguments from console.settings import Settings from console.store import Store PREVIEW = NAME + "-preview-20261013" def inspect(name): return json.loads(command("docker", "inspect", name))[0] def get(port, path): req = urllib.request.Request(f"http://10.88.118.188:{port}{path}", headers={"Accept": "application/json"}) with urllib.request.urlopen(req, timeout=8) as response: return json.load(response) def main(): prepare() preview = inspect(NAME) if preview["Config"]["Labels"].get("xas.application") != "script-console": raise RuntimeError("Unexpected preview owner") if command("docker", "ps", "-aq", "--filter", "name=^/" + PREVIEW + "$"): raise RuntimeError("Cutover has already been attempted; inspect saved state") for name in ("adamwen-test-console", "adamwen-test-console-proxy"): if inspect(name)["Config"]["Labels"].get("cn.aisuan.owner") != "adamwen-test-console": raise RuntimeError("Unexpected old console owner") old_runs = get(14440, "/api/41/project/xas-test-library/executions/running") if not isinstance(old_runs.get("executions"), list): raise RuntimeError("Cannot verify old console running tasks") if get(14442, "/api/summary")["active"] or old_runs["executions"]: raise RuntimeError("Console has running work; wait before cutover") store = Store(Settings.from_env()) if not any(r["state"] == "succeeded" and r["config"]["profile"] == "legacy_smoke" for r in store.runs()): raise RuntimeError("Successful single smoke acceptance missing") backup = ROOT / "runtime/backups/before-dedicated-20261013" backup.mkdir(mode=0o700, exist_ok=False) command("systemctl", "stop", UNIT) command("docker", "stop", NAME) with sqlite3.connect(store.settings.db) as source, sqlite3.connect(backup / "console.sqlite3") as destination: source.backup(destination) if destination.execute("PRAGMA integrity_check").fetchone()[0] != "ok": raise RuntimeError("Snapshot integrity failure") for folder in ("library", "runs"): shutil.copytree(ROOT / "runtime" / folder, backup / folder) (backup / "metadata.json").write_text(json.dumps({"observed_at": datetime.now(timezone.utc).isoformat(), "preview_image": preview["Image"], "new_image": IMAGE, "old_console": "adamwen-test-console", "old_proxy": "adamwen-test-console-proxy"}, indent=2)) command("docker", "rename", NAME, PREVIEW) command("docker", "stop", "adamwen-test-console") command("docker", "stop", "adamwen-test-console-proxy") try: print(command(*web_arguments(14440)), flush=True) command("systemctl", "start", UNIT) for _ in range(20): try: if get(14440, "/api/summary")["worker"]["online"]: print("CUTOVER_OK", flush=True) break except (OSError, ValueError): pass time.sleep(.5) else: raise RuntimeError("New entry point failed readiness check") except Exception: # Only revert app availability, never restore old data over newer records. result = subprocess.run(["docker", "inspect", NAME], capture_output=True, text=True) if result.returncode == 0 and json.loads(result.stdout)[0]["Config"]["Labels"].get("xas.application") == "script-console": command("docker", "rm", "-f", NAME) command("docker", "start", "adamwen-test-console-proxy") command("docker", "start", "adamwen-test-console") command("docker", "start", PREVIEW) command("systemctl", "start", UNIT) raise if __name__ == "__main__": main()