feat(deploy): 目录自适应 + Celery 自动轮询接入 systemd
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 运行期文件。
This commit is contained in:
@@ -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 <<EOF
|
||||
[Unit]
|
||||
Description=IPAM Backend API (uvicorn)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$BACKEND_DIR
|
||||
Environment=PYTHONPATH=$BACKEND_DIR
|
||||
ExecStart=$BACKEND_DIR/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port $API_PORT
|
||||
Restart=always
|
||||
RestartSec=3
|
||||
StandardOutput=append:$LOG_DIR/ipam-backend.log
|
||||
StandardError=append:$LOG_DIR/ipam-backend.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# --- Celery Worker ---
|
||||
cat > /etc/systemd/system/$SYSTEMD_CELERY_WORKER.service <<EOF
|
||||
[Unit]
|
||||
Description=IPAM Celery Worker
|
||||
After=$SYSTEMD_BACKEND.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$BACKEND_DIR
|
||||
Environment=PYTHONPATH=$BACKEND_DIR
|
||||
ExecStart=$BACKEND_DIR/venv/bin/celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:$LOG_DIR/ipam-celery-worker.log
|
||||
StandardError=append:$LOG_DIR/ipam-celery-worker.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# --- Celery Beat ---
|
||||
cat > /etc/systemd/system/$SYSTEMD_CELERY_BEAT.service <<EOF
|
||||
[Unit]
|
||||
Description=IPAM Celery Beat Scheduler
|
||||
After=$SYSTEMD_CELERY_WORKER.service network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=$BACKEND_DIR
|
||||
Environment=PYTHONPATH=$BACKEND_DIR
|
||||
ExecStart=$BACKEND_DIR/venv/bin/celery -A app.tasks.celery_app beat --loglevel=info
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=append:$LOG_DIR/ipam-celery-beat.log
|
||||
StandardError=append:$LOG_DIR/ipam-celery-beat.log
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
# enable 开机自启
|
||||
systemctl enable $SYSTEMD_BACKEND >/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
|
||||
|
||||
Reference in New Issue
Block a user