#!/usr/bin/env python3 """Deploy popadmin IM messages UI (timestamp + date range) to static-override. Also optionally rebuilds spark-server for start/end query support. """ from __future__ import annotations import os import sys import tarfile import tempfile import time from pathlib import Path import paramiko ROOT = Path(__file__).resolve().parents[2] HOST = "47.236.66.9" USER = "root" PEM = Path(os.path.expandvars(r"%USERPROFILE%\.ssh\wyrs_shanghai_deploy.pem")) LOCAL_JS = ROOT / "deploy" / "popadmin" / "popadmin.js" REMOTE_STATIC = "/opt/wyrs-admin/static-override" CACHE = time.strftime("20261102-imts-%H%M%S") REMOTE_BUILD = "/tmp/spark-im-messages-build" INCLUDE = ["cmd", "internal", "migrations", "api", "go.mod", "go.sum"] EXTRA_FILES = [ (ROOT / "deploy" / "docker" / "Dockerfile.remote", "Dockerfile.remote"), (ROOT / "deploy" / "docker" / "deploy_remote.sh", "deploy_remote.sh"), ] def load_key(path: Path): for loader in ( paramiko.Ed25519Key.from_private_key_file, paramiko.RSAKey.from_private_key_file, paramiko.ECDSAKey.from_private_key_file, ): try: return loader(str(path)) except Exception: continue raise SystemExit(f"cannot load private key {path}") def run(client: paramiko.SSHClient, cmd: str, timeout: int = 180) -> tuple[int, str]: print("$", cmd[:240]) _, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode("utf-8", "replace") err = stderr.read().decode("utf-8", "replace") code = stdout.channel.recv_exit_status() text = (out + (("\n" + err) if err.strip() else "")).strip() if text: print(text[-12000:]) return code, text def deploy_static(client: paramiko.SSHClient, sftp: paramiko.SFTPClient) -> None: ts = time.strftime("%Y%m%d%H%M%S") print("=== backup + upload static ===") run( client, f"cp -a {REMOTE_STATIC}/popadmin.js {REMOTE_STATIC}/popadmin.js.bak.imts.{ts} && " f"cp -a {REMOTE_STATIC}/popadmin.index.html {REMOTE_STATIC}/popadmin.index.html.bak.imts.{ts}", ) sftp.put(str(LOCAL_JS), f"{REMOTE_STATIC}/popadmin.js") idx_cmd = f"""python3 - <<'PY' from pathlib import Path import re p = Path('{REMOTE_STATIC}/popadmin.index.html') s = p.read_text(encoding='utf-8') s2 = re.sub(r'popadmin\\.js\\?v=[^\"\\']+', 'popadmin.js?v={CACHE}', s) p.write_text(s2, encoding='utf-8') print('cache', '{CACHE}') print('js_ok', 'popadmin.js?v={CACHE}' in s2) print('has_fmtMsgTs', 'fmtMsgTs' in Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8', errors='replace')) print('has_imPeriod', 'imPeriod' in Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8', errors='replace')) PY""" code, _ = run(client, idx_cmd) if code != 0: raise SystemExit(f"cache bump failed: {code}") def make_tarball(path: Path) -> None: with tarfile.open(path, "w:gz") as tar: for name in INCLUDE: p = ROOT / name if not p.exists(): raise SystemExit(f"missing {p}") tar.add(p, arcname=name) for src, arc in EXTRA_FILES: if not src.is_file(): raise SystemExit(f"missing {src}") tar.add(src, arcname=arc) print("tarball", path, path.stat().st_size, "bytes") def deploy_backend(client: paramiko.SSHClient, sftp: paramiko.SFTPClient) -> None: release = "im-range-" + time.strftime("%Y%m%d%H%M%S") print("=== conflict probe ===") code, out = run( client, "for f in /tmp/spark-image-limit-build.lock /tmp/spark-compose-mutate.lock " "/tmp/sign-energy-deploy.lock /tmp/spark-im-messages-build.lock; do " " if [ -f \"$f\" ]; then echo LOCKED:$f; cat \"$f\"; exit 99; fi; " "done; echo locks_clear", timeout=60, ) if code != 0: raise SystemExit(f"deploy locked: {out}") with tempfile.TemporaryDirectory() as td: tar_path = Path(td) / "spark-src.tgz" make_tarball(tar_path) run(client, f"rm -rf {REMOTE_BUILD}; mkdir -p {REMOTE_BUILD}", timeout=60) print("upload tarball...") sftp.put(str(tar_path), f"{REMOTE_BUILD}/spark-src.tgz") for src, arc in EXTRA_FILES: data = src.read_bytes().replace(b"\r\n", b"\n") with sftp.file(f"{REMOTE_BUILD}/{arc}", "wb") as f: f.write(data) try: f.chmod(0o755 if arc.endswith(".sh") else 0o644) except Exception: pass print("=== remote docker build ===") code, out = run( client, f"cd {REMOTE_BUILD} && chmod +x deploy_remote.sh && " f"SPARK_RELEASE={release} SPARK_SRC_DIR={REMOTE_BUILD} " f"SPARK_DEPLOY_LOCK=/tmp/spark-im-messages-build.lock " f"./deploy_remote.sh", timeout=1800, ) if code != 0: raise SystemExit(f"backend deploy failed: {code}") # probe new string in binary run( client, "docker exec spark-server sh -c " "\"strings /usr/local/bin/spark-server | grep -F '查询跨度最长' | head -3\" " "|| true", timeout=60, ) def verify(client: paramiko.SSHClient) -> None: print("=== verify ===") run( client, "python3 - <<'PY'\n" "from pathlib import Path\n" f"js=Path('{REMOTE_STATIC}/popadmin.js').read_text(encoding='utf-8',errors='replace')\n" f"idx=Path('{REMOTE_STATIC}/popadmin.index.html').read_text(encoding='utf-8',errors='replace')\n" "print('fmtMsgTs', 'fmtMsgTs' in js)\n" "print('imPeriod', 'imPeriod' in js)\n" "print('exceedsMonths', 'exceedsMonths' in js)\n" "print('start_end_q', 'start=' in js and 'imPeriod' in js)\n" f"print('cache_line', [ln for ln in idx.splitlines() if 'popadmin.js' in ln][:2])\n" "PY", timeout=60, ) # API auth gate still 401; bad range after auth would 400 — check route alive run( client, "curl -sS -o /tmp/im_msg.out -w '%{http_code}' " "'http://127.0.0.1:18080/api/admin/spark/im/messages?limit=1&start=2020-01-01&end=2021-02-01'; " "echo; head -c 200 /tmp/im_msg.out; echo", timeout=30, ) def main() -> int: if not PEM.is_file(): print("missing pem", PEM) return 1 if not LOCAL_JS.is_file(): print("missing", LOCAL_JS) return 1 do_backend = "--skip-backend" not in sys.argv client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, username=USER, pkey=load_key(PEM), timeout=30) sftp = client.open_sftp() try: deploy_static(client, sftp) if do_backend: deploy_backend(client, sftp) else: print("skip backend") verify(client) print("DEPLOY DONE cache=", CACHE) finally: sftp.close() client.close() return 0 if __name__ == "__main__": raise SystemExit(main())