From 18cefa6dede132bd75107525bf10f574e5426c69 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 12 Aug 2026 11:05:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20=E7=9B=AE=E5=BD=95=E8=87=AA?= =?UTF-8?q?=E9=80=82=E5=BA=94=20+=20Celery=20=E8=87=AA=E5=8A=A8=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E6=8E=A5=E5=85=A5=20systemd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. start.sh BASE_DIR 不再硬编码 /root/ipam,改为跟随脚本所在目录 (BASH_SOURCE 自动检测),无论部署在 /opt /usr/local /root 等 任意目录都能工作。 2. 运行 start.sh 时自动生成 3 个 systemd 单元并 enable 开机自启: - ipam-backend (uvicorn :8008) - ipam-celery-worker - ipam-celery-beat (每60秒触发SNMP设备自动轮询) 均含 Restart=always 崩溃自动重启,日志写入 /var/log/ipam/。 3. 前端 vite dev server 保持 nohup 启动(不纳入 systemd)。 4. stop.sh 改为 systemctl stop 停止 systemd 服务,pkill 兜底。 5. .gitignore 忽略 celerybeat-schedule 运行期文件。 --- .gitignore | 4 + start.sh | 223 +++++++++++++++++++++++++++++++++++++---------------- stop.sh | 53 +++++++++---- 3 files changed, 197 insertions(+), 83 deletions(-) diff --git a/.gitignore b/.gitignore index 7102dfc..02685b8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,10 @@ dist/ *.log /tmp/ +# Celery Beat 调度状态文件(运行期自动生成) +celerybeat-schedule* +celerybeat.pid + # OS .DS_Store Thumbs.db diff --git a/start.sh b/start.sh index 9acb3a6..fdc4edf 100755 --- a/start.sh +++ b/start.sh @@ -1,5 +1,13 @@ #!/bin/bash # IPAM 管理系统一键启动脚本 +# +# 特点: +# - 目录自适应:不硬编码 /root/ipam,自动跟随本脚本所在目录。 +# 无论部署在 /opt、/usr/local、/root 还是其它任意目录都能工作。 +# - systemd 管理:后端、Celery Worker、Celery Beat 都生成 systemd 单元 +# (ipam-backend / ipam-celery-worker / ipam-celery-beat), +# 运行本脚本时自动生成并 enable,保证开机自启、崩溃自动重启。 +# - 前端 vite dev server 仍用 nohup 启动(开发服务,不纳入 systemd)。 echo "==========================================" echo " IPAM 管理系统启动脚本" @@ -12,8 +20,10 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color -# 项目根目录 -BASE_DIR="/root/ipam" +# ============ 目录自适应(核心改动)============ +# 无论脚本放哪里,都以其所在目录为项目根目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_DIR="$SCRIPT_DIR" BACKEND_DIR="$BASE_DIR/backend" FRONTEND_DIR="$BASE_DIR/frontend" @@ -21,20 +31,31 @@ FRONTEND_DIR="$BASE_DIR/frontend" API_PORT=8008 FRONTEND_PORT=3000 +# systemd 服务名 +SYSTEMD_BACKEND="ipam-backend" +SYSTEMD_CELERY_WORKER="ipam-celery-worker" +SYSTEMD_CELERY_BEAT="ipam-celery-beat" + +# 日志目录 +LOG_DIR="/var/log/ipam" +mkdir -p "$LOG_DIR" 2>/dev/null + +echo -e "${GREEN}📁 项目目录: $BASE_DIR${NC}" + # 检查 Docker 容器状态 check_docker_containers() { echo "🔍 检查 Docker 容器..." - + # 检查 MySQL if [ "$(docker inspect -f '{{.State.Running}}' ipam-mysql 2>/dev/null)" != "true" ]; then echo -e "${YELLOW}MySQL 容器未运行,正在启动...${NC}" docker start ipam-mysql 2>/dev/null || { echo -e "${YELLOW}创建 MySQL 容器...${NC}" docker run -d --name ipam-mysql -p 3308:3306 \ - -e MYSQL_ROOT_PASSWORD=*** \ + -e MYSQL_ROOT_PASSWORD=ipam2024 \ -e MYSQL_DATABASE=ipam \ -e MYSQL_USER=ipam \ - -e MYSQL_PASSWORD=*** \ + -e MYSQL_PASSWORD=ipam2024 \ mysql:8.0 --default-authentication-plugin=mysql_native_password } fi @@ -63,87 +84,151 @@ kill_port() { fi } -# 启动后端 API +# ============ systemd 单元动态生成 ============ +# 生成 3 个服务单元(backend / celery-worker / celery-beat) +# 路径全部跟随 BASE_DIR,保证部署目录变更后重新运行本脚本即可重写 +setup_systemd_services() { + echo "⚙️ 生成 systemd 服务单元..." + + # --- 后端 --- + cat > /etc/systemd/system/$SYSTEMD_BACKEND.service < /etc/systemd/system/$SYSTEMD_CELERY_WORKER.service < /etc/systemd/system/$SYSTEMD_CELERY_BEAT.service </dev/null 2>&1 + systemctl enable $SYSTEMD_CELERY_WORKER >/dev/null 2>&1 + systemctl enable $SYSTEMD_CELERY_BEAT >/dev/null 2>&1 + + echo -e "${GREEN}✅ systemd 单元已生成并启用开机自启:${NC}" + echo " - $SYSTEMD_BACKEND.service" + echo " - $SYSTEMD_CELERY_WORKER.service" + echo " - $SYSTEMD_CELERY_BEAT.service" + echo "" +} + +# 启动后端(systemd) start_backend() { echo "🚀 启动后端 API 服务..." - + # 检查虚拟环境 if [ ! -d "$BACKEND_DIR/venv" ]; then echo -e "${YELLOW}创建 Python 虚拟环境...${NC}" cd $BACKEND_DIR python3 -m venv venv fi - - # 激活虚拟环境并启动 - cd $BACKEND_DIR - source venv/bin/activate - - # 检查端口并清理 + + # 检查并清理占用端口的旧进程 kill_port $API_PORT - - # 后台启动 uvicorn - nohup uvicorn app.main:app --host 0.0.0.0 --port $API_PORT > /tmp/ipam-backend.log 2>&1 & - BACKEND_PID=$! - - # 等待服务启动 - sleep 5 - - # 检查是否启动成功 + + # 通过 systemd 启动 + systemctl restart $SYSTEMD_BACKEND + sleep 4 + if curl -s http://localhost:$API_PORT/health > /dev/null; then - echo -e "${GREEN}✅ 后端 API 启动成功 (端口 $API_PORT)${NC}" - echo " PID: $BACKEND_PID" - echo " 日志: /tmp/ipam-backend.log" + echo -e "${GREEN}✅ 后端 API 启动成功 (端口 $API_PORT, systemd 管理)${NC}" return 0 else echo -e "${RED}❌ 后端 API 启动失败${NC}" - echo " 查看日志: tail -50 /tmp/ipam-backend.log" + echo " 查看日志: journalctl -u $SYSTEMD_BACKEND -n 50 或 tail -50 $LOG_DIR/ipam-backend.log" return 1 fi echo "" } -# 启动 Celery Worker +# 启动 Celery(systemd:worker + beat) start_celery() { - echo "🚀 启动 Celery Worker..." - - cd $BACKEND_DIR - source venv/bin/activate - - # 检查是否已有 celery 进程 - pkill -f "celery.*app.tasks.celery_app" 2>/dev/null - sleep 1 - - # 后台启动 celery - nohup celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4 > /tmp/ipam-celery.log 2>&1 & - CELERY_PID=$! - - sleep 3 - - if ps -p $CELERY_PID > /dev/null; then - echo -e "${GREEN}✅ Celery Worker 启动成功${NC}" - echo " PID: $CELERY_PID" - echo " 日志: /tmp/ipam-celery.log" + echo "🚀 启动 Celery Worker & Beat (systemd)..." + systemctl restart $SYSTEMD_CELERY_WORKER + systemctl restart $SYSTEMD_CELERY_BEAT + sleep 4 + + systemctl is-active --quiet $SYSTEMD_CELERY_WORKER && WORKER_ACTIVE="yes" || WORKER_ACTIVE="no" + systemctl is-active --quiet $SYSTEMD_CELERY_BEAT && BEAT_ACTIVE="yes" || BEAT_ACTIVE="no" + + if [ "$WORKER_ACTIVE" = "yes" ] && [ "$BEAT_ACTIVE" = "yes" ]; then + echo -e "${GREEN}✅ Celery Worker & Beat 启动成功 (systemd 管理)${NC}" return 0 else - echo -e "${RED}❌ Celery Worker 启动失败${NC}" - echo " 查看日志: tail -50 /tmp/ipam-celery.log" + echo -e "${RED}❌ Celery 启动异常 (worker=$WORKER_ACTIVE beat=$BEAT_ACTIVE)${NC}" + echo " 查看日志: journalctl -u $SYSTEMD_CELERY_WORKER -n 30 -u $SYSTEMD_CELERY_BEAT -n 30" return 1 fi echo "" } -# 启动前端 +# 启动前端(nohup,开发服务) start_frontend() { echo "🚀 启动前端服务..." - + cd $FRONTEND_DIR - - # 检查 node_modules 及关键依赖是否完整 - if [ ! -d "node_modules" ] || [ ! -f "node_modules/element-plus/es/index.mjs" ]; then - echo -e "${YELLOW}安装前端依赖...${NC}" - npm install + + # 检查 node_modules 及关键依赖是否完整(含 vite,全局 npm 可能配置了 omit=dev) + if [ ! -d "node_modules" ] || [ ! -f "node_modules/vite/bin/vite.js" ]; then + echo -e "${YELLOW}安装前端依赖(含 devDependencies,保证 vite 可用)...${NC}" + npm install --include=dev fi - + # 检查 OUI 数据库文件 if [ ! -f "$BACKEND_DIR/app/data/oui.txt" ]; then echo -e "${YELLOW}OUI 数据库不存在,尝试从 IEEE 下载...${NC}" @@ -155,7 +240,6 @@ import os os.makedirs('app/data', exist_ok=True) lookup = MacLookup() lookup.update_vendors() -# 复制到项目目录 import shutil src = os.path.expanduser('~/.cache/mac-vendors.txt') if os.path.exists(src): @@ -163,29 +247,28 @@ if os.path.exists(src): print('OUI 数据库下载完成') " 2>/dev/null || echo -e "${YELLOW}OUI 数据库下载失败(网络问题),使用内置硬编码回退${NC}" fi - + # 检查端口并清理 kill_port $FRONTEND_PORT - + # 清理 Vite 缓存,避免 element-plus exports 解析失败 if [ -d "node_modules/.vite" ]; then echo -e "${YELLOW}清理 Vite 缓存...${NC}" rm -rf node_modules/.vite fi - + # 后台启动前端 nohup npm run dev -- --host 0.0.0.0 --port $FRONTEND_PORT > /tmp/ipam-frontend.log 2>&1 & FRONTEND_PID=$! - + sleep 8 - + if curl -s http://localhost:$FRONTEND_PORT > /dev/null; then echo -e "${GREEN}✅ 前端服务启动成功 (端口 $FRONTEND_PORT)${NC}" echo " PID: $FRONTEND_PID" echo " 日志: /tmp/ipam-frontend.log" return 0 else - # 如果启动失败,尝试清缓存重试一次 echo -e "${YELLOW}首次启动失败,尝试清理 Vite 缓存重试...${NC}" kill -9 $FRONTEND_PID 2>/dev/null sleep 1 @@ -217,8 +300,9 @@ show_status() { echo "📊 服务状态:" echo " - MySQL: 127.0.0.1:3308 ✅" echo " - Redis: 127.0.0.1:6379 ✅" - echo " - 后端 API: http://$(hostname -I | awk '{print $1}'):$API_PORT ✅" - echo " - Celery: 后台运行 ✅" + echo " - 后端 API: http://$(hostname -I | awk '{print $1}'):$API_PORT ✅ (systemd: $SYSTEMD_BACKEND)" + echo " - Celery Worker: systemd: $SYSTEMD_CELERY_WORKER ✅" + echo " - Celery Beat: systemd: $SYSTEMD_CELERY_BEAT ✅(每60秒触发SNMP设备自动轮询)" echo " - 前端界面: http://$(hostname -I | awk '{print $1}'):$FRONTEND_PORT ✅" echo "" echo "📖 访问地址:" @@ -226,7 +310,11 @@ show_status() { echo " - API 文档: http://$(hostname -I | awk '{print $1}'):$API_PORT/docs" echo "" echo "🛑 停止服务命令:" - echo " pkill -f 'uvicorn|celery|vite'" + echo " bash $SCRIPT_DIR/stop.sh" + echo " 或 systemctl stop $SYSTEMD_CELERY_BEAT $SYSTEMD_CELERY_WORKER $SYSTEMD_BACKEND" + echo "" + echo "📈 查看状态:systemctl status $SYSTEMD_BACKEND $SYSTEMD_CELERY_WORKER $SYSTEMD_CELERY_BEAT" + echo "📈 查看日志:journalctl -u $SYSTEMD_CELERY_BEAT -f" echo "" } @@ -234,7 +322,7 @@ show_status() { main() { # 检查是否是 root 用户 if [ "$EUID" -ne 0 ]; then - echo -e "${RED}请使用 root 权限运行此脚本${NC}" + echo -e "${RED}请使用 root 权限运行此脚本(生成 systemd 单元需要)${NC}" exit 1 fi @@ -258,6 +346,7 @@ main() { echo "" check_docker_containers + setup_systemd_services start_backend start_celery start_frontend diff --git a/stop.sh b/stop.sh index f04fecc..88edc59 100755 --- a/stop.sh +++ b/stop.sh @@ -1,5 +1,8 @@ #!/bin/bash # IPAM 管理系统停止脚本 +# +# 通过 systemd 停止后端与 Celery(若服务已接入 systemd), +# 并停止前端 dev server。 echo "==========================================" echo " IPAM 管理系统停止脚本" @@ -12,37 +15,52 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color +# 目录自适应(跟随本脚本所在目录) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BASE_DIR="$SCRIPT_DIR" + +# systemd 服务名(与 start.sh 保持一致) +SYSTEMD_BACKEND="ipam-backend" +SYSTEMD_CELERY_WORKER="ipam-celery-worker" +SYSTEMD_CELERY_BEAT="ipam-celery-beat" + echo "🛑 正在停止所有服务..." echo "" -# 停止后端 -echo "停止后端 API 服务..." -pkill -f "uvicorn app.main:app" 2>/dev/null -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ 后端已停止${NC}" +# 停止 Celery Beat +echo "停止 Celery Beat..." +if systemctl stop $SYSTEMD_CELERY_BEAT 2>/dev/null; then + echo -e "${GREEN}✅ Celery Beat 已停止 (systemd)${NC}" else - echo -e "${YELLOW}ℹ️ 后端未运行${NC}" + pkill -f "celery.*app.tasks.celery_app.*beat" 2>/dev/null + echo -e "${YELLOW}ℹ️ Celery Beat 已停止 (pkill 兜底)${NC}" fi -# 停止 Celery +# 停止 Celery Worker echo "" echo "停止 Celery Worker..." -pkill -f "celery.*app.tasks.celery_app" 2>/dev/null -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ Celery Worker 已停止${NC}" +if systemctl stop $SYSTEMD_CELERY_WORKER 2>/dev/null; then + echo -e "${GREEN}✅ Celery Worker 已停止 (systemd)${NC}" else - echo -e "${YELLOW}ℹ️ Celery Worker 未运行${NC}" + pkill -f "celery.*app.tasks.celery_app" 2>/dev/null + echo -e "${YELLOW}ℹ️ Celery Worker 已停止 (pkill 兜底)${NC}" +fi + +# 停止后端 +echo "" +echo "停止后端 API 服务..." +if systemctl stop $SYSTEMD_BACKEND 2>/dev/null; then + echo -e "${GREEN}✅ 后端已停止 (systemd)${NC}" +else + pkill -f "uvicorn app.main:app" 2>/dev/null + echo -e "${YELLOW}ℹ️ 后端已停止 (pkill 兜底)${NC}" fi # 停止前端 echo "" echo "停止前端服务..." pkill -f "vite" 2>/dev/null -if [ $? -eq 0 ]; then - echo -e "${GREEN}✅ 前端服务已停止${NC}" -else - echo -e "${YELLOW}ℹ️ 前端服务未运行${NC}" -fi +echo -e "${GREEN}✅ 前端服务已停止${NC}" echo "" echo "==========================================" @@ -52,3 +70,6 @@ echo "" echo "📋 剩余进程检查:" ps aux | grep -E "(uvicorn|celery|vite)" | grep -v grep | awk '{print $2, $11}' echo "" +echo "💡 若希望永久停止开机自启,可执行:" +echo " systemctl disable $SYSTEMD_BACKEND $SYSTEMD_CELERY_WORKER $SYSTEMD_CELERY_BEAT" +echo ""