19 KiB
deploy.py nginx 站点启用超时修复 — 实施计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 修复 python deploy.py 中 nginx 站点启用命令 30s 超时问题,通过将链式 ln -snf && rm -f 拆为 5 步独立 SSH 调用 + 前置诊断清理。
Architecture: 在 deploy.py 中新增 enable_nginx_site(remote_conf, domain) 函数(5 步骤:诊断→清理→建链→清默认→验证),替换 deploy_nginx 第 250-253 行的链式命令。每步独立 ssh_command(timeout=30) 调用,失败立即停止;变量入口校验防御 shell 注入。
Tech Stack: Python 3.10+, subprocess (ssh/scp), bash test/case/ln 内置命令
文件结构
修改:G:\IdeaProjects\emotion-museun\deploy.py
- 新增函数
enable_nginx_site(在deploy_nginx之前,约 70 行) - 修改
deploy_nginx(第 250-256 行,替换链式命令为函数调用)
不修改:
ssh_command()/run_ssh_args()/scp_file()底层conf/emotion-museum.confnginx 配置文件- 其他 deploy.py 子脚本
Task 1: 添加 enable_nginx_site 函数骨架 + 变量安全校验
Files:
-
Modify:
G:\IdeaProjects\emotion-museun\deploy.py:230-266(在deploy_nginx前插入新函数) -
Step 1: 在
deploy_nginx之前插入新函数骨架
Step 1a:先在文件顶部 imports 区(第 23-28 行附近)追加 import re:
将 deploy.py 第 23-28 行:
import io
import os
import sys
import time
import subprocess
from pathlib import Path
改为:
import io
import os
import re
import sys
import time
import subprocess
from pathlib import Path
Step 1b:在 deploy.py 第 230 行(# Nginx 配置 注释之前)插入以下代码:
# ============================================================================
# Nginx 站点启用(防御性诊断 + 清理)
# ============================================================================
# 变量安全校验正则
_SAFE_DOMAIN = re.compile(r'^[a-zA-Z0-9.-]+$')
_SAFE_REMOTE_CONF = re.compile(r'^[a-zA-Z0-9./_-]+$')
def _validate_nginx_inputs(remote_conf: str, domain: str) -> bool:
"""校验 domain 和 remote_conf 防止 shell 注入和路径遍历。
Returns: True 合法;False 非法(已 log_error)
"""
if not domain or not _SAFE_DOMAIN.match(domain):
log_error(f"非法 domain 名称: {domain!r}")
return False
if domain.startswith('-') or domain.endswith('-') \
or domain.startswith('.') or domain.endswith('.') \
or '..' in domain:
log_error(f"非法 domain 结构: {domain!r} (首尾连字符/点 或 连续点非法)")
return False
if not remote_conf or not _SAFE_REMOTE_CONF.match(remote_conf):
log_error(f"非法 remote_conf 路径: {remote_conf!r}")
return False
if '//' in remote_conf or '..' in remote_conf:
log_error(f"remote_conf 包含可疑路径片段: {remote_conf!r}")
return False
return True
def enable_nginx_site(remote_conf: str, domain: str) -> bool:
"""幂等地启用 nginx 站点(sites-enabled symlink)。
流程:
1. 诊断目标路径当前状态(symlink/dir/file/missing)
2. 清理异常状态(目录/文件/断链);失败短路
3. 二次检查 + 创建 symlink
4. 移除 default 站点(失败仅 warning)
5. 验证 symlink + default 状态
Returns: True 成功;False 失败(stderr 已打印)
"""
if not _validate_nginx_inputs(remote_conf, domain):
return False
target = f"/etc/nginx/sites-enabled/{domain}.conf"
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
return True # 后续 Task 替换为完整 5 步实现
- Step 2: 验证 Python 语法正确
Run: cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"
Expected: 退出码 0,无输出(语法正确)
- Step 3: 验证变量校验逻辑
Run: cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/sites-available/lifescript.happylifeos.com.conf', 'lifescript.happylifeos.com'))"
Expected: 输出 True
Run: cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/sites-available/test.conf', '-evil.com'))"
Expected: 输出 False(同时 stderr 出现 [ERROR] 非法 domain 结构)
Run: cd G:\IdeaProjects\emotion-museun && python -c "from deploy import _validate_nginx_inputs; print(_validate_nginx_inputs('/etc/nginx/../etc/passwd', 'example.com'))"
Expected: 输出 False(同时 stderr 出现 [ERROR] remote_conf 包含可疑路径片段)
- Step 4: Commit
cd G:\IdeaProjects\emotion-museun
git add deploy.py
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 添加 enable_nginx_site 函数骨架 + 变量安全校验"
Task 2: 实现步骤 1(诊断)+ 步骤 2(清理,case 注入 status)
Files:
-
Modify:
G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site函数体 -
Step 1: 替换
enable_nginx_site末尾的return True为 5 步骤实现(部分)
将 enable_nginx_site 函数末尾的:
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
return True # 后续 Task 替换为完整 5 步实现
替换为:
log_info(f"启用 nginx 站点: {target} -> {remote_conf}")
# ===== 步骤 1: 诊断目标路径状态 =====
log_info("诊断步骤: 检查目标路径...")
diagnose_cmd = (
f'test -e {target} && '
f'(test -L {target} && echo "symlink" || '
f'(test -d {target} && echo "dir" || echo "file")) || '
f'echo "missing"'
)
ok, status, err = ssh_command(diagnose_cmd, timeout=30)
if not ok:
log_error(f"诊断步骤失败: {err}")
return False
status = status.strip()
log_info(f"诊断结果: {status}")
# ===== 步骤 2: 清理异常状态 =====
log_info("清理步骤: 处理异常状态...")
cleanup_cmd = (
f'case "{status}" in '
f'symlink) unlink {target} ;; '
f'dir) rm -rf {target} ;; '
f'file) rm -f {target} ;; '
f'missing) ;; '
f'esac'
)
ok, _, err = ssh_command(cleanup_cmd, timeout=30)
if not ok:
log_error(f"清理步骤失败: {err}")
log_error("上下文: 目标 = " + target)
log_error("提示: 权限可能不足(检查 /etc/nginx/sites-enabled 写权限)")
return False
log_info("清理步骤完成")
return True # Task 3 替换为完整建链+清默认
- Step 2: 验证 Python 语法
Run: cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"
Expected: 退出码 0
- Step 3: 验证 SSH 命令构造正确(不执行 SSH)
Run: cd G:\IdeaProjects\emotion-museun && python -c " from deploy import _validate_nginx_inputs target = '/etc/nginx/sites-enabled/lifescript.happylifeos.com.conf' status = 'dir' diagnose_cmd = f'test -e {target} && (test -L {target} && echo symlink || (test -d {target} && echo dir || echo file)) || echo missing' cleanup_cmd = f'case \"{status}\" in symlink) unlink {target} ;; dir) rm -rf {target} ;; file) rm -f {target} ;; missing) ;; esac' print('DIAGNOSE:', diagnose_cmd) print('CLEANUP:', cleanup_cmd) "
Expected:
DIAGNOSE: test -e /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && (test -L /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && echo symlink || (test -d /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && echo dir || echo file)) || echo missing
CLEANUP: case "dir" in symlink) unlink /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; dir) rm -rf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; file) rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf ;; missing) ;; esac
- Step 4: Commit
cd G:\IdeaProjects\emotion-museun
git add deploy.py
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 1(诊断)+ 步骤 2(清理)"
Task 3: 实现步骤 3(建链,两段式)+ 步骤 4(清 default)
Files:
-
Modify:
G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site函数体 -
Step 1: 在清理步骤后插入步骤 3 和 4
将 enable_nginx_site 函数末尾的 return True # Task 3 替换为完整建链+清默认 替换为:
log_info("清理步骤完成")
# ===== 步骤 3: 建链(两段式:先二次检查,再 ln -snf) =====
log_info("建链步骤: 创建 symlink...")
# 第一段:二次检查 target 不存在(防并发进程干扰)
guard_cmd = f'if [ -e {target} ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi'
ok, _, err = ssh_command(guard_cmd, timeout=30)
if not ok:
log_error(f"建链步骤失败 - 二次检查: {err}")
log_error("提示: 并发进程干扰(其他进程在步骤 2 后、步骤 3 前创建了同名项)")
log_error("上下文: 目标 = " + target)
return False
# 第二段:执行 ln -snf(独立 SSH 调用,错误信息不会被二次检查误报)
link_cmd = f'ln -snf {remote_conf} {target}'
ok, _, err = ssh_command(link_cmd, timeout=30)
if not ok:
log_error(f"建链步骤失败 - ln 执行: {err}")
log_error("提示: 确认 scp_file 是否成功上传了 sites-available 下的 conf 文件")
log_error("上下文: 目标 = " + target)
return False
log_info("建链步骤完成")
# ===== 步骤 4: 清理 default 站点(失败仅 warning) =====
log_info("清默认步骤: 移除 default 站点...")
ok, _, err = ssh_command("rm -f /etc/nginx/sites-enabled/default", timeout=30)
if not ok:
log_warn(f"清默认步骤失败(不阻塞): {err}")
log_warn("提示: default 文件可能仍存在,nginx 可能有端口冲突;步骤 5 验证会确认")
else:
log_info("清默认步骤完成")
return True # Task 4 替换为完整 5 步(含验证)
- Step 2: 验证 Python 语法
Run: cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"
Expected: 退出码 0
- Step 3: 验证 SSH 命令构造(两段式)
Run: cd G:\IdeaProjects\emotion-museun && python -c " target = '/etc/nginx/sites-enabled/test.com.conf' remote_conf = '/etc/nginx/sites-available/test.com.conf' guard_cmd = f'if [ -e {target} ]; then echo \"二次检查失败:target 仍存在\" >&2; exit 1; fi' link_cmd = f'ln -snf {remote_conf} {target}' print('GUARD:', guard_cmd) print('LINK:', link_cmd) "
Expected:
GUARD: if [ -e /etc/nginx/sites-enabled/test.com.conf ]; then echo "二次检查失败:target 仍存在" >&2; exit 1; fi
LINK: ln -snf /etc/nginx/sites-available/test.com.conf /etc/nginx/sites-enabled/test.com.conf
- Step 4: Commit
cd G:\IdeaProjects\emotion-museun
git add deploy.py
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 3(两段式建链)+ 步骤 4(清 default)"
Task 4: 实现步骤 5(验证 symlink + default)+ 集成到 deploy_nginx
Files:
-
Modify:
G:\IdeaProjects\emotion-museun\deploy.py:enable_nginx_site函数体(末尾) -
Modify:
G:\IdeaProjects\emotion-museun\deploy.py:249-256(deploy_nginx 内部) -
Step 1: 在
enable_nginx_site末尾插入步骤 5
将 enable_nginx_site 函数末尾的 return True # Task 4 替换为完整 5 步(含验证) 替换为:
else:
log_info("清默认步骤完成")
# ===== 步骤 5: 验证结果 =====
log_info("验证步骤: 检查 symlink 和 default 状态...")
verify_cmd = (
f'readlink {target} && '
f'(test ! -e /etc/nginx/sites-enabled/default && echo "default:OK" || echo "default:STILL_EXISTS") && '
f'ls -la /etc/nginx/sites-enabled/'
)
ok, stdout, err = ssh_command(verify_cmd, timeout=30)
if not ok:
log_error(f"验证步骤失败: {err}")
log_error(f"实际输出: {stdout}")
return False
log_info("验证结果:")
for line in stdout.split('\n'):
if line.strip():
log_info(f" {line}")
log_info("启用站点配置完成")
return True
- Step 2: 修改
deploy_nginx,替换链式命令为函数调用
将 deploy.py 第 249-256 行的:
log_info("启用站点配置...")
ok, _, err = ssh_command(
f"ln -snf {remote_conf} /etc/nginx/sites-enabled/{DOMAIN}.conf "
f"&& rm -f /etc/nginx/sites-enabled/default"
)
if not ok:
log_error(f"启用站点失败: {err}")
return False
替换为:
log_info("启用站点配置...")
if not enable_nginx_site(remote_conf, DOMAIN):
log_error("启用站点失败(详见 enable_nginx_site 内部日志)")
return False
- Step 3: 验证 Python 语法
Run: cd G:\IdeaProjects\emotion-museun && python -c "import ast; ast.parse(open('deploy.py', encoding='utf-8').read())"
Expected: 退出码 0
- Step 4: 验证函数可正常导入
Run: cd G:\IdeaProjects\emotion-museun && python -c "from deploy import enable_nginx_site, _validate_nginx_inputs; print('import OK')"
Expected: 输出 import OK
- Step 5: 验证函数签名和文档
Run: cd G:\IdeaProjects\emotion-museun && python -c " from deploy import enable_nginx_site import inspect sig = inspect.signature(enable_nginx_site) print('Signature:', sig) print('Docstring (first line):', enable_nginx_site.__doc__.split(chr(10))[0]) "
Expected:
Signature: (remote_conf: str, domain: str) -> bool
Docstring (first line): 幂等地启用 nginx 站点(sites-enabled symlink)。
- Step 6: Commit
cd G:\IdeaProjects\emotion-museun
git add deploy.py
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "feat(deploy): 实现 enable_nginx_site 步骤 5(验证)+ 集成到 deploy_nginx"
Task 5: 端到端验证(CLAUDE.md 强制规则)
Files:
-
无文件修改
-
Step 1: SSH 连接预检
Run: cd G:\IdeaProjects\emotion-museun && python deploy.py verify 2>&1 | head -30
Expected: SSH 连接成功(看到 [INFO] SSH 连接正常)
若失败:
-
检查
~/.ssh/config是否有101.200.208.45免密配置 -
手动
ssh root@101.200.208.45 echo ok测试 -
Step 2: 执行 nginx 部署
Run: cd G:\IdeaProjects\emotion-museun && python deploy.py nginx 2>&1 | tee C:\Users\peanu\AppData\Local\Temp\opencode\nginx-deploy-validation.txt
Expected: 完整流程成功,无 30s 超时;看到:
[INFO] 部署 Nginx 配置
[INFO] 上传 Nginx 配置文件...
[INFO] 启用站点配置...
[INFO] 启用 nginx 站点: /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf -> /etc/nginx/sites-available/lifescript.happylifeos.com.conf
[INFO] 诊断步骤: 检查目标路径...
[INFO] 诊断结果: <symlink|dir|file|missing>
[INFO] 清理步骤: 处理异常状态...
[INFO] 清理步骤完成
[INFO] 建链步骤: 创建 symlink...
[INFO] 建链步骤完成
[INFO] 清默认步骤: 移除 default 站点...
[INFO] 清默认步骤完成
[INFO] 验证步骤: 检查 symlink 和 default 状态...
[INFO] 验证结果:
[INFO] <readlink 输出>
[INFO] default:OK
[INFO] <ls -la 输出>
[INFO] 启用站点配置完成
[INFO] 验证 Nginx 配置...
[INFO] Nginx 配置重载完成
- Step 3: 验证远程 nginx 状态
Run: cd G:\IdeaProjects\emotion-museun && python -c "from deploy import ssh_command; print(ssh_command('ls -la /etc/nginx/sites-enabled/', timeout=15))"
Expected: 输出 /etc/nginx/sites-enabled/ 目录列表,包含有效 symlink(如 lifescript.happylifeos.com.conf -> /etc/nginx/sites-available/lifescript.happylifeos.com.conf),不包含 default
- Step 4: 验证 nginx 服务可访问
Run: curl -I https://lifescript.happylifeos.com/ 2>&1 | head -5
Expected: HTTP 200 或 301/302(站点可达)
- Step 5: Commit 验证记录
cd G:\IdeaProjects\emotion-museun
git add -A
git -c user.name="deploy-bot" -c user.email="bot@local" commit -m "test: 端到端验证 nginx 部署成功(见 opencode temp 验证日志)" --allow-empty
验证场景(设计文档要求)
以下场景可在 Task 5 验证后可选执行(手动复现异常状态):
场景 A: 断链场景
ssh root@101.200.208.45 "ln -s /nonexistent /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
Expected: 诊断结果 = symlink,步骤 2 用 unlink 清理,步骤 3 重建成功
场景 B: 目录场景
ssh root@101.200.208.45 "rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf && mkdir /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
Expected: 诊断结果 = dir,步骤 2 用 rm -rf 清理,步骤 3 重建成功
场景 C: 干净场景
ssh root@101.200.208.45 "rm -f /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
Expected: 诊断结果 = missing,跳过清理,步骤 3 直接建链
场景 D: 正常 symlink 场景(幂等性)
ssh root@101.200.208.45 "ln -snf /etc/nginx/sites-available/lifescript.happylifeos.com.conf /etc/nginx/sites-enabled/lifescript.happylifeos.com.conf"
cd G:\IdeaProjects\emotion-museun && python deploy.py nginx
Expected: 诊断结果 = symlink,步骤 2 用 unlink,步骤 3 重建(幂等:重复执行结果相同)
回滚
如新代码有问题:
cd G:\IdeaProjects\emotion-museun
git log --oneline deploy.py # 找到本次 4 个 commit
git revert <commit-hash>..HEAD # 批量回滚
# 或:
git checkout HEAD~4 -- deploy.py # 恢复到本次修改前
风险提醒
- 首次部署若远程 nginx 配置异常:清理步骤可能误删用户手动创建的目录 → 建议首次部署前先 SSH 检查
/etc/nginx/sites-enabled/状态 - SSH 免密登录失效:所有 ssh_command 调用都会失败,Task 5 Step 1 会捕获
- 网络慢导致 30s 超时:单步 30s 对常规操作足够;如遇网络问题可临时改为
timeout=60(在调用处覆盖)
后续可选增强(不在本次范围)
disable_nginx_site()反向操作- 提取到独立模块
deploy_nginx_helpers.py - 增加
--dry-run模式 - 步骤级重试机制