b6df028839
变更摘要: - [fix] 清理根目录 index.html 遗留代码(questionnaire-error + retryLoad 移除) - [fix] 副标题同步更新为自估591分 + 候选志愿546条数据校准 - [fix] 页脚注入版本号 v1.0.0 - [fix] 部署版 index.html: 取消家长标签,统一 filler-tianheng - [fix] Dashboard 文案修正(4份→2份线上问卷) - [fix] 清理冗余部署脚本(v1-v4归档至 archived_scripts/) - [fix] 等效位次工具纳入 deploy 目录 - [chore] .gitignore 初始化 - [init] Git 仓库初始化
179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
"""
|
|
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()
|