Files
happy-life-star/deploy.py
T
peanut f1b31fc9c0 重构:移除部署脚本中的 SSL 证书操作
SSL 证书已配置完成,不再需要从部署脚本中执行 certbot。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 19:29:49 +08:00

379 lines
12 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
情绪博物馆 - 统一部署脚本
合并 deploy-all.sh / deploy-to-prod.sh / deploy-domain.sh
使用方法:
python deploy.py # 部署所有服务
python deploy.py backend # 仅部署后端
python deploy.py frontend # 仅部署前端
python deploy.py admin # 仅部署管理后台
python deploy.py life-script # 仅部署 Life-Script
python deploy.py ssl # 仅申请 SSL 证书
python deploy.py nginx # 仅部署 Nginx 配置
python deploy.py verify # 仅验证部署结果
python deploy.py all # 部署所有服务(同无参数)
Author: Peanut
Created: 2026-05-17
Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shell 脚本
"""
import io
import os
import sys
import time
import subprocess
from pathlib import Path
# 强制 stdout/stderr 使用 UTF-8 编码,避免 Windows GBK 编码错误
if hasattr(sys.stdout, 'buffer'):
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
# ============================================================================
# 配置
# ============================================================================
SERVER_IP = "101.200.208.45"
USERNAME = "root"
DOMAIN = "lifescript.happylifeos.com"
# 项目根目录(脚本所在目录)
PROJECT_DIR = Path(__file__).parent.absolute()
SSH_OPTS = f"-o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=no"
# ============================================================================
# 日志
# ============================================================================
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m'
def log_info(msg):
print(f"{Colors.GREEN}[INFO]{Colors.NC} {msg}")
def log_warn(msg):
print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}")
def log_error(msg):
print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")
def log_section(msg):
print(f"\n{Colors.BLUE}{'=' * 60}{Colors.NC}")
print(f"{Colors.BLUE}{msg}{Colors.NC}")
print(f"{Colors.BLUE}{'=' * 60}{Colors.NC}\n")
# ============================================================================
# 工具函数
# ============================================================================
def run_command(cmd, cwd=None, timeout=120, capture=True):
"""执行本地命令"""
try:
if capture:
result = subprocess.run(
cmd, shell=True, cwd=cwd,
capture_output=True, text=True, encoding='utf-8', errors='replace',
timeout=timeout
)
return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
else:
result = subprocess.run(
cmd, shell=True, cwd=cwd, timeout=timeout
)
return result.returncode == 0, "", ""
except subprocess.TimeoutExpired:
return False, "", f"命令执行超时 ({timeout}s): {cmd}"
except Exception as e:
return False, "", str(e)
def ssh_command(cmd, timeout=30):
"""在远程服务器执行命令"""
full = f'ssh {SSH_OPTS} {USERNAME}@{SERVER_IP} "{cmd}"'
return run_command(full, timeout=timeout)
def scp_file(local, remote, timeout=120):
"""上传文件到远程服务器"""
full = f'scp {SSH_OPTS} "{local}" {USERNAME}@{SERVER_IP}:"{remote}"'
return run_command(full, timeout=timeout)
def check_ssh():
"""检查 SSH 连接"""
log_info("检查 SSH 连接...")
success, stdout, stderr = ssh_command("echo ok", timeout=15)
if not success:
log_error(f"SSH 连接失败,请先配置免密登录: ssh {USERNAME}@{SERVER_IP}")
if stderr:
log_error(f"错误详情: {stderr}")
sys.exit(1)
log_info("SSH 连接正常")
# ============================================================================
# Nginx 配置
# ============================================================================
def deploy_nginx():
"""部署 Nginx 配置"""
log_section("部署 Nginx 配置")
nginx_conf = PROJECT_DIR / "conf" / "emotion-museum.conf"
if not nginx_conf.exists():
log_error(f"Nginx 配置文件不存在: {nginx_conf}")
return False
log_info("上传 Nginx 配置文件...")
remote_conf = f"/etc/nginx/sites-available/{DOMAIN}.conf"
# 先创建远程目录
ssh_command("mkdir -p /etc/nginx/sites-available /etc/nginx/sites-enabled", timeout=15)
ok, _, err = scp_file(str(nginx_conf), remote_conf, timeout=60)
if not ok:
log_error(f"上传 Nginx 配置失败: {err}")
return False
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("验证 Nginx 配置...")
ok, _, err = ssh_command("nginx -t", timeout=15)
if ok:
ssh_command("systemctl reload nginx", timeout=30)
log_info("Nginx 配置重载完成")
return True
else:
log_error(f"Nginx 配置验证失败: {err}")
return False
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh remote
# ============================================================================
def deploy_backend():
"""部署后端服务"""
log_section("部署后端服务")
deploy_script = PROJECT_DIR / "backend-single" / "deploy.py"
if not deploy_script.exists():
log_error(f"后端部署脚本不存在: {deploy_script}")
return False
log_info("执行后端部署...")
ok, _, err = run_command(
"python3 deploy.py remote",
cwd=str(PROJECT_DIR / "backend-single"),
timeout=600
)
if ok:
log_info("后端部署完成")
return True
else:
log_error(f"后端部署失败: {err}")
return False
# ============================================================================
# 前端 - 调用 web/deploy.sh
# ============================================================================
def deploy_frontend():
"""部署用户前端"""
log_section("部署用户前端")
deploy_script = PROJECT_DIR / "web" / "deploy.py"
if not deploy_script.exists():
log_error(f"前端部署脚本不存在: {deploy_script}")
return False
log_info("执行前端部署...")
ok, _, err = run_command(
"python3 deploy.py",
cwd=str(PROJECT_DIR / "web"),
timeout=600
)
if ok:
log_info("前端部署完成")
return True
else:
log_error(f"前端部署失败: {err}")
return False
# ============================================================================
# 管理后台 - 调用 web-admin/deploy.py
# ============================================================================
def deploy_admin():
"""部署管理后台"""
log_section("部署管理后台")
deploy_script = PROJECT_DIR / "web-admin" / "deploy.py"
if not deploy_script.exists():
log_error(f"管理后台部署脚本不存在: {deploy_script}")
return False
log_info("执行管理后台部署...")
ok, _, err = run_command(
"python3 deploy.py",
cwd=str(PROJECT_DIR / "web-admin"),
timeout=600
)
if ok:
log_info("管理后台部署完成")
return True
else:
log_error(f"管理后台部署失败: {err}")
return False
# ============================================================================
# Life-Script - 调用 life-script/deploy.sh
# ============================================================================
def deploy_life_script():
"""部署 Life-Script"""
log_section("部署 Life-Script")
deploy_script = PROJECT_DIR / "life-script" / "deploy.py"
if not deploy_script.exists():
log_error(f"Life-Script 部署脚本不存在: {deploy_script}")
return False
log_info("执行 Life-Script 部署...")
ok, _, err = run_command(
"python3 deploy.py",
cwd=str(PROJECT_DIR / "life-script"),
timeout=600
)
if ok:
log_info("Life-Script 部署完成")
return True
else:
log_error(f"Life-Script 部署失败: {err}")
return False
# ============================================================================
# 验证部署
# ============================================================================
def verify_deploy():
"""验证部署结果"""
log_section("验证部署")
log_info("检查 HTTPS 访问...")
endpoints = [
(f"https://{DOMAIN}/", "前端页面"),
(f"https://{DOMAIN}/emotion-museum-admin/", "管理后台"),
(f"https://{DOMAIN}/api/", "API 代理"),
(f"http://{DOMAIN}/", "HTTP 跳转"),
]
for url, label in endpoints:
ok, stdout, _ = run_command(
f'curl -k -s -o /dev/null -w "HTTP %{{http_code}}" {url}',
timeout=30
)
if ok and stdout:
log_info(f" {label}: {stdout}")
else:
log_warn(f" {label}: 访问异常")
log_info("验证完成")
return True
# ============================================================================
# 主程序
# ============================================================================
def print_usage():
"""打印使用说明"""
print("情绪博物馆 - 统一部署脚本")
print()
print("使用方法:")
print(" python deploy.py # 部署所有服务")
print(" python deploy.py backend # 仅部署后端")
print(" python deploy.py frontend # 仅部署前端")
print(" python deploy.py admin # 仅部署管理后台")
print(" python deploy.py life-script # 仅部署 Life-Script")
print(" python deploy.py ssl # 仅申请 SSL 证书")
print(" python deploy.py nginx # 仅部署 Nginx 配置")
print(" python deploy.py verify # 仅验证部署结果")
print(" python deploy.py all # 部署所有服务(同无参数)")
def main():
deploy_type = sys.argv[1] if len(sys.argv) > 1 else "all"
if deploy_type in ("--help", "-h", "help"):
print_usage()
return
start_time = time.time()
log_section("情绪博物馆 - 统一部署")
log_info(f"部署类型: {deploy_type}")
log_info(f"部署时间: {time.strftime('%Y-%m-%d %H:%M:%S')}")
actions = {
"backend": lambda: check_ssh() or True and deploy_backend(),
"frontend": lambda: check_ssh() or True and deploy_frontend(),
"admin": lambda: check_ssh() or True and deploy_admin(),
"life-script": lambda: check_ssh() or True and deploy_life_script(),
"nginx": lambda: check_ssh() or True and deploy_nginx(),
"verify": lambda: verify_deploy(),
"all": None, # handled separately
}
if deploy_type == "all":
check_ssh()
deploy_nginx() or True
deploy_backend()
deploy_frontend()
deploy_admin()
deploy_life_script()
verify_deploy()
elif deploy_type in actions:
actions[deploy_type]()
else:
log_error(f"无效的部署类型: {deploy_type}")
print("使用方法: python deploy.py [backend|frontend|admin|life-script|nginx|verify|all]")
sys.exit(1)
duration = int(time.time() - start_time)
log_section(f"部署完成 (耗时 {duration}s)")
log_info(f"用户前端: https://{DOMAIN}/")
log_info(f"管理后台: https://{DOMAIN}/emotion-museum-admin/")
log_info(f"Life-Script: https://{DOMAIN}/life-script/")
log_info(f"API 地址: https://{DOMAIN}/api")
log_info(f"WebSocket: wss://{DOMAIN}/ws")
if __name__ == "__main__":
main()