Files
gitlab-instance-0a899031_do…/tools/deploy_live_paramiko.py

270 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""通过 ParamikoSFTP + exec部署到 ARM 板,无交互 SSH。日志写入 .deploy_logs/。"""
from __future__ import annotations
import os
import posixpath
import sys
import time
from datetime import datetime
from pathlib import Path
try:
import paramiko
except ImportError:
print("请先安装: pip install paramiko", file=sys.stderr)
sys.exit(1)
HOST = os.environ.get("LIVE_DEPLOY_HOST", "192.168.21.105")
USER = os.environ.get("LIVE_DEPLOY_USER", "live")
PASS = os.environ.get("LIVE_DEPLOY_PASS", "12345678")
REMOTE_BASE = os.environ.get("LIVE_DEPLOY_PATH", "/home/live/douyinyoutube").rstrip("/")
REPO_ROOT = Path(__file__).resolve().parent.parent
FILES = [
"src/control_plane.py",
"src/web_process_backend.py",
"web.py",
"main.py",
"scripts/hardware_probe.py",
"douyin_youtube_ffplay.py",
"web-console/package.json",
"web-console/package-lock.json",
"web-console/app/globals.css",
"web-console/app/layout.tsx",
"web-console/app/page.tsx",
"web-console/middleware.ts",
"web-console/components/OverviewCharts.tsx",
]
LOG_DIR = REPO_ROOT / ".deploy_logs"
TAIL_LINES = 90
def _safe_print(text: str) -> None:
try:
print(text)
except UnicodeEncodeError:
enc = getattr(sys.stdout, "encoding", None) or "utf-8"
try:
sys.stdout.buffer.write(text.encode(enc, errors="replace") + b"\n")
except Exception:
sys.stdout.buffer.write(text.encode("ascii", errors="backslashreplace") + b"\n")
def _decode(data: bytes) -> str:
if not data:
return ""
try:
return data.decode("utf-8", errors="replace")
except Exception:
return data.decode("ascii", errors="backslashreplace")
def _tail(text: str, n: int = TAIL_LINES) -> str:
lines = text.splitlines()
if len(lines) <= n:
return text
return "\n".join(lines[-n:])
def _log_append(path: Path, text: str) -> None:
with path.open("a", encoding="utf-8", errors="replace") as fh:
fh.write(text)
if not text.endswith("\n"):
fh.write("\n")
def mkdir_p(sftp: paramiko.SFTPClient, remote_dir: str) -> None:
remote_dir = remote_dir.replace("\\", "/").rstrip("/")
if not remote_dir or remote_dir == "/":
return
parts = [p for p in remote_dir.split("/") if p]
cur = ""
for p in parts:
cur = f"{cur}/{p}" if cur else f"/{p}"
try:
sftp.stat(cur)
except OSError:
try:
sftp.mkdir(cur)
except OSError:
pass
def sftp_put(sftp: paramiko.SFTPClient, local: Path, remote: str) -> None:
remote = remote.replace("\\", "/")
parent = posixpath.dirname(remote)
mkdir_p(sftp, parent)
sftp.put(str(local), remote)
def connect() -> tuple[paramiko.SSHClient, paramiko.SFTPClient]:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
HOST,
username=USER,
password=PASS,
timeout=30,
banner_timeout=40,
auth_timeout=30,
)
return client, client.open_sftp()
def run_raw(ssh: paramiko.SSHClient, cmd: str, timeout: int) -> tuple[int, str, str]:
# 非交互;统一 UTF-8减少远端 locale 乱码
wrapped = (
"export LC_ALL=C.UTF-8 LANG=C.UTF-8; "
"cd ~ 2>/dev/null || true; "
+ cmd
)
_stdin, stdout, stderr = ssh.exec_command(wrapped, timeout=timeout, get_pty=False)
out_b = stdout.read()
err_b = stderr.read()
code = stdout.channel.recv_exit_status()
return code, _decode(out_b), _decode(err_b)
def run_logged(
ssh: paramiko.SSHClient,
log_path: Path,
title: str,
cmd: str,
timeout: int = 120,
) -> tuple[int, str]:
block = f"\n========== {title} ==========\n$ {cmd}\n"
_log_append(log_path, block)
code, out, err = run_raw(ssh, cmd, timeout=timeout)
full = f"exit={code}\n--- stdout ---\n{out}\n--- stderr ---\n{err}\n"
_log_append(log_path, full)
_safe_print(f"\n--- {title} (最后 {TAIL_LINES} 行) ---\n{_tail(full)}")
return code, full
def main() -> int:
LOG_DIR.mkdir(parents=True, exist_ok=True)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_path = LOG_DIR / f"deploy_{stamp}.log"
log_path.write_text(f"deploy {stamp} -> {USER}@{HOST}:{REMOTE_BASE}\n", encoding="utf-8")
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
_safe_print(f"完整日志: {log_path}")
client, sftp = None, None
try:
client, sftp = connect()
except Exception as e:
_safe_print(f"连接失败: {e}")
_log_append(log_path, f"CONNECT FAIL: {e!r}\n")
return 1
try:
run_logged(client, log_path, "pwd", f"cd {REMOTE_BASE} && pwd", timeout=30)
run_logged(client, log_path, "id", "id", timeout=15)
run_logged(client, log_path, "hostname", "hostname", timeout=15)
run_logged(
client,
log_path,
"repo_exists",
f"test -d {REMOTE_BASE} && echo ok || echo MISSING",
timeout=15,
)
for rel in FILES:
lp = REPO_ROOT / rel
if not lp.is_file():
_safe_print(f"本地缺少文件: {lp}")
return 2
rp = f"{REMOTE_BASE}/{rel.replace(chr(92), '/')}"
_log_append(log_path, f"SFTP PUT {lp} -> {rp}\n")
sftp_put(sftp, lp, rp)
_safe_print(f"已上传: {rel}")
py_compile = (
f"cd {REMOTE_BASE} && python3 -m py_compile web.py "
"src/control_plane.py src/web_process_backend.py "
"scripts/hardware_probe.py douyin_youtube_ffplay.py main.py"
)
code_py, _ = run_logged(client, log_path, "python_py_compile", py_compile, timeout=90)
if code_py != 0:
_safe_print("远程 py_compile 失败,见日志")
return 3
build_cmd = (
f"cd {REMOTE_BASE}/web-console && "
"(npm ci 2>/dev/null || npm install) && "
"(command -v pnpm >/dev/null 2>&1 && pnpm run build || npm run build)"
)
code_b, _ = run_logged(client, log_path, "web_console_build", build_cmd, timeout=900)
if code_b != 0:
_safe_print("[警告] web-console build 非 0可能未装 Node/pnpm完整输出见日志")
sudo_snap = (
f"echo '{PASS}' | sudo -S bash -c "
f"'systemctl list-units --type=service --state=running 2>/dev/null | head -50 || true'"
)
run_logged(client, log_path, "sudo_systemctl_running_head", sudo_snap, timeout=45)
restart_cmd = (
f"echo '{PASS}' | sudo -S bash -c '"
"systemctl try-restart live-console.service 2>/dev/null || true; "
"systemctl try-restart douyinyoutube.service 2>/dev/null || true; "
"systemctl try-restart douyin-live.service 2>/dev/null || true; "
"systemctl try-restart live-web.service 2>/dev/null || true; "
"echo try_restart_done'"
)
run_logged(client, log_path, "sudo_try_restart_units", restart_cmd, timeout=60)
run_logged(
client,
log_path,
"sudo_restart_live_console",
f"echo '{PASS}' | sudo -S systemctl restart live-console.service",
timeout=120,
)
_safe_print("等待服务监听…")
time.sleep(6)
run_logged(
client,
log_path,
"curl_health_after_restart",
"curl -sS -m 15 http://127.0.0.1:8001/health 2>&1 || "
"curl -sS -m 15 http://127.0.0.1:8101/health 2>&1 || "
"echo 'curl_health_failed'",
timeout=25,
)
run_logged(
client,
log_path,
"pm2_or_process",
f"command -v pm2 >/dev/null 2>&1 && pm2 list || "
f"(pgrep -af 'launch.py|uvicorn|web.py' | head -15 || true)",
timeout=30,
)
run_logged(
client,
log_path,
"curl_server_info",
"curl -sS -m 10 'http://127.0.0.1:8001/server_info' 2>&1 | head -c 4000 || true",
timeout=20,
)
return 0
finally:
if sftp:
sftp.close()
if client:
client.close()
if __name__ == "__main__":
raise SystemExit(main())