""" NAS 部署脚本 v5 —— 修复 Docker 全路径 + 直接启动 """ import paramiko import os import sys import time import base64 import tarfile import io NAS_HOST = "100.85.152.112" NAS_PORT = 22 NAS_USER = "simon" NAS_PASS = "Nas82829330" NAS_DEPLOY_PATH = "/volume1/docker/gaokao-portal" LOCAL_DEPLOY_DIR = r"C:\Users\simon\WorkBuddy\2026-05-20-15-22-53\deploy" # 群晖上 Docker 的实际路径(不在 sudo secure_path 中) DOCKER_BIN = "/usr/local/bin/docker" SUDO_DOCKER = f"echo '{NAS_PASS}' | sudo -S {DOCKER_BIN}" def connect(): client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(NAS_HOST, port=NAS_PORT, username=NAS_USER, password=NAS_PASS, timeout=15) return client def run_cmd(client, cmd, desc="", timeout=30): if desc: print(f"[执行] {desc}") disp = cmd[:150] + ('...' if len(cmd) > 150 else '') print(f" $ {disp}") stdin, stdout, stderr = client.exec_command(cmd, timeout=timeout) out = stdout.read().decode('utf-8', errors='replace') err = stderr.read().decode('utf-8', errors='replace') status = stdout.channel.recv_exit_status() if out.strip(): for line in out.strip().split('\n')[:30]: print(f" > {line}") if err.strip(): for line in err.strip().split('\n')[:10]: if 'sudo' not in line.lower() or status != 0: print(f" ! {line}") return out.strip(), err.strip(), status def run_cmd_stdin(client, cmd, stdin_data, desc="", timeout=60): """通过 channel 的 stdin 传输数据""" if desc: print(f"[传输] {desc}") transport = client.get_transport() channel = transport.open_session() channel.exec_command(cmd) chunk_size = 8192 total = len(stdin_data) sent = 0 while sent < total: chunk = stdin_data[sent:sent+chunk_size] channel.sendall(chunk) sent += len(chunk) channel.shutdown_write() out = b"" err = b"" while not channel.exit_status_ready(): if channel.recv_ready(): out += channel.recv(4096) if channel.recv_stderr_ready(): err += channel.recv_stderr(4096) time.sleep(0.05) while channel.recv_ready(): out += channel.recv(4096) while channel.recv_stderr_ready(): err += channel.recv_stderr(4096) out_str = out.decode('utf-8', errors='replace') err_str = err.decode('utf-8', errors='replace') status = channel.recv_exit_status() if out_str.strip(): for line in out_str.strip().split('\n')[:10]: print(f" > {line}") if err_str.strip() and status != 0: for line in err_str.strip().split('\n')[:5]: print(f" ! {line}") print(f" 传输: {sent} 字节 -> 退出码 {status}") return out_str, err_str, status def main(): print("=" * 60) print(" 高考志愿填报门户 - NAS 部署 v5") print("=" * 60) # 1. 打包 print("\n─── 打包文件 ───") buf = io.BytesIO() with tarfile.open(fileobj=buf, mode="w:gz") as tar: tar.add(LOCAL_DEPLOY_DIR, arcname=".") tar_data = buf.getvalue() print(f" 打包: {len(tar_data)/1024:.1f} KB") # 2. base64 b64_data = base64.b64encode(tar_data).decode('ascii') # 3. 连接 client = connect() print("[连接] SSH OK") try: # 4. 测试 Docker 全路径 print("\n─── Docker 检查 ───") out, _, status = run_cmd(client, f"{SUDO_DOCKER} --version", "Docker 版本") if status != 0: print("[错误] Docker 不可用") return run_cmd(client, f"{SUDO_DOCKER} compose version", "Compose 版本") # 5. 传输文件 print("\n─── 传输文件 ───") decode_cmd = "base64 -d > /tmp/gaokao-deploy.tar.gz 2>/dev/null && echo DECODE_OK || echo DECODE_FAIL" out, _, status = run_cmd_stdin(client, decode_cmd, b64_data.encode('ascii'), "Base64 传输") # 6. 解压 print("\n─── 解压部署 ───") run_cmd(client, f"rm -rf '{NAS_DEPLOY_PATH}' && mkdir -p '{NAS_DEPLOY_PATH}'", "准备目录") run_cmd(client, f"tar -xzf /tmp/gaokao-deploy.tar.gz -C '{NAS_DEPLOY_PATH}'", "解压") run_cmd(client, f"find '{NAS_DEPLOY_PATH}' -type f | wc -l", "文件计数") # 7. 修改 docker-compose.yml 端口(8080 空闲,直接用 8080) # 确认 nginx.conf 端口 run_cmd(client, f"grep -n 'listen' '{NAS_DEPLOY_PATH}/nginx/nginx.conf'", "nginx 监听端口") # 8. 启动 Docker Compose print("\n─── 启动 Docker Compose ───") compose = f"{SUDO_DOCKER} compose" # 先停旧容器 run_cmd(client, f"cd '{NAS_DEPLOY_PATH}' && {compose} down 2>&1", "停止旧容器") time.sleep(2) # 构建并启动(拉取 nginx 镜像 + 构建后端) print(" 构建镜像并启动(可能需要几分钟)...") out, err, status = run_cmd(client, f"cd '{NAS_DEPLOY_PATH}' && {compose} up -d --build 2>&1", "docker compose up", timeout=180) # 等启动 print("\n等待容器就绪...") for i in range(10): time.sleep(6) out, _, _ = run_cmd(client, f"cd '{NAS_DEPLOY_PATH}' && {compose} ps 2>&1", f"检查 {i+1}/10") if "Up" in out: print(" 容器已运行!") break if i == 5: # 看看日志 run_cmd(client, f"cd '{NAS_DEPLOY_PATH}' && {compose} logs --tail=20 2>&1", "中途日志") else: print("[警告] 容器可能未正常启动,检查日志...") # 9. 日志 print("\n─── 完整日志 ───") run_cmd(client, f"cd '{NAS_DEPLOY_PATH}' && {compose} logs --tail=50 2>&1", "容器日志") # 10. HTTP 测试 print("\n─── HTTP 测试 ───") run_cmd(client, "curl -s http://localhost:8080/ | head -20", "首页内容") # 11. 清理 run_cmd(client, "rm -f /tmp/gaokao-deploy.tar.gz", "清理") print("\n" + "=" * 60) print(" 部署完成!") print(f" 内网: http://100.85.152.112:8080") print("=" * 60) finally: client.close() if __name__ == "__main__": main()