Add Paramiko deploy/verify scripts for ARM board; gitignore deploy logs
Made-with: Cursor
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -172,6 +172,9 @@ node_modules/
|
||||
# Next 开发缓存(静态导出目录 web-console/out 可纳入版本库以便无 Node 环境直接部署)
|
||||
web-console/.next/
|
||||
|
||||
# Paramiko 部署本地日志(可能含敏感命令回显)
|
||||
.deploy_logs/
|
||||
|
||||
# 无 PM2 时的本地进程状态
|
||||
.process_runner/
|
||||
|
||||
|
||||
252
tools/deploy_live_paramiko.py
Normal file
252
tools/deploy_live_paramiko.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""通过 Paramiko(SFTP + exec)部署到 ARM 板,无交互 SSH。日志写入 .deploy_logs/。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
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/web_process_backend.py",
|
||||
"web.py",
|
||||
"main.py",
|
||||
"scripts/hardware_probe.py",
|
||||
"douyin_youtube_ffplay.py",
|
||||
"web-console/app/page.tsx",
|
||||
"web-console/middleware.ts",
|
||||
]
|
||||
|
||||
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/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 && "
|
||||
"(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,
|
||||
"curl_health",
|
||||
"curl -sS -m 10 http://127.0.0.1:8001/health 2>&1 || "
|
||||
"curl -sS -m 10 http://127.0.0.1:8101/health 2>&1 || "
|
||||
"echo 'curl_health_failed'",
|
||||
timeout=20,
|
||||
)
|
||||
|
||||
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())
|
||||
57
tools/verify_live_console.py
Normal file
57
tools/verify_live_console.py
Normal file
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
"""重启 live-console 并验收 HTTP(Paramiko)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import paramiko
|
||||
|
||||
HOST = "192.168.21.105"
|
||||
USER = "live"
|
||||
PASS = "12345678"
|
||||
LOG = Path(__file__).resolve().parent.parent / ".deploy_logs" / "verify_live_console.log"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
LOG.parent.mkdir(parents=True, exist_ok=True)
|
||||
c = paramiko.SSHClient()
|
||||
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
c.connect(HOST, username=USER, password=PASS, timeout=30)
|
||||
|
||||
def run(cmd: str, timeout: int = 90) -> tuple[int, str, str]:
|
||||
_, stdout, stderr = c.exec_command(
|
||||
"export LC_ALL=C.UTF-8 LANG=C.UTF-8; " + cmd,
|
||||
timeout=timeout,
|
||||
)
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
code = stdout.channel.recv_exit_status()
|
||||
block = f"\n$ {cmd}\nexit={code}\n--- out ---\n{out}\n--- err ---\n{err}\n"
|
||||
with LOG.open("a", encoding="utf-8", errors="replace") as fh:
|
||||
fh.write(block)
|
||||
tail = "\n".join(block.splitlines()[-60:])
|
||||
print(tail)
|
||||
return code, out, err
|
||||
|
||||
try:
|
||||
run(
|
||||
f"echo '{PASS}' | sudo -S systemctl restart live-console.service",
|
||||
timeout=120,
|
||||
)
|
||||
time.sleep(5)
|
||||
run("systemctl is-active live-console.service", timeout=20)
|
||||
run("ss -tlnp 2>/dev/null | grep 8001 || true", timeout=20)
|
||||
run(
|
||||
"curl -sS -m 15 http://127.0.0.1:8001/health && echo "
|
||||
"&& curl -sS -m 15 http://127.0.0.1:8001/server_info | head -c 1200",
|
||||
timeout=40,
|
||||
)
|
||||
return 0
|
||||
finally:
|
||||
c.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user