deploy_dataops.sh 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/env bash
  2. #
  3. # DataOps Platform 一键部署脚本
  4. # 在 deployment/ 目录(或解压后的发布包根目录)内执行
  5. #
  6. # 用法:
  7. # sudo bash deploy_dataops.sh
  8. # sudo ENABLE_NGINX=1 NGINX_SERVER_NAME=company.citupro.com NGINX_LISTEN_PORT=18183 bash deploy_dataops.sh
  9. #
  10. set -euo pipefail
  11. APP_NAME="${APP_NAME:-dataops-platform}"
  12. APP_DIR="${APP_DIR:-/opt/dataops-platform}"
  13. APP_USER="${APP_USER:-ubuntu}"
  14. APP_GROUP="${APP_GROUP:-$APP_USER}"
  15. ENV_DIR="${ENV_DIR:-/etc/dataops-platform}"
  16. ENV_FILE="${ENV_FILE:-$ENV_DIR/dataops.env}"
  17. LISTEN_HOST="${LISTEN_HOST:-0.0.0.0}"
  18. LISTEN_PORT="${LISTEN_PORT:-5500}"
  19. GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
  20. GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}"
  21. ENABLE_NGINX="${ENABLE_NGINX:-0}"
  22. NGINX_SERVER_NAME="${NGINX_SERVER_NAME:-company.citupro.com}"
  23. NGINX_LISTEN_PORT="${NGINX_LISTEN_PORT:-18183}"
  24. NGINX_SSL="${NGINX_SSL:-1}"
  25. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  26. SUDO=""
  27. if [[ "$(id -u)" -ne 0 ]]; then
  28. SUDO="sudo"
  29. fi
  30. RED='\033[0;31m'
  31. GREEN='\033[0;32m'
  32. YELLOW='\033[1;33m'
  33. BLUE='\033[0;34m'
  34. NC='\033[0m'
  35. log() { printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"; }
  36. info() { echo -e "${GREEN}[INFO]${NC} $*"; }
  37. warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
  38. err() { echo -e "${RED}[ERROR]${NC} $*"; }
  39. step() { echo -e "${BLUE}[STEP]${NC} $*"; }
  40. require_command() {
  41. if ! command -v "$1" >/dev/null 2>&1; then
  42. err "缺少命令: $1"
  43. exit 1
  44. fi
  45. }
  46. normalize_text_files() {
  47. local path
  48. for path in "$@"; do
  49. [[ -e "${path}" ]] || continue
  50. if [[ -d "${path}" ]]; then
  51. find "${path}" -type f \( -name '*.sh' -o -name '*.env' -o -name '.env*' \) -print0 2>/dev/null \
  52. | while IFS= read -r -d '' file; do sed -i 's/\r$//' "${file}" 2>/dev/null || true; done
  53. elif [[ -f "${path}" ]]; then
  54. sed -i 's/\r$//' "${path}" 2>/dev/null || true
  55. fi
  56. done
  57. }
  58. require_command python3
  59. require_command supervisorctl
  60. for item in app requirements.txt wsgi.py gunicorn_config.py scripts/run_dataops.sh; do
  61. if [[ ! -e "${SCRIPT_DIR}/${item}" ]]; then
  62. err "部署包不完整,缺少: ${SCRIPT_DIR}/${item}"
  63. err "请确认在 deployment/ 或发布包根目录内执行本脚本"
  64. exit 1
  65. fi
  66. done
  67. install_env_file() {
  68. step "配置环境变量 ${ENV_FILE}"
  69. $SUDO install -d -m 750 "${ENV_DIR}"
  70. if [[ -f "${ENV_FILE}" ]]; then
  71. info "使用已有环境变量: ${ENV_FILE}"
  72. normalize_text_files "${ENV_FILE}"
  73. return 0
  74. fi
  75. if [[ -f "${SCRIPT_DIR}/dataops.env" ]]; then
  76. $SUDO install -m 640 "${SCRIPT_DIR}/dataops.env" "${ENV_FILE}"
  77. info "已从 dataops.env 安装到 ${ENV_FILE}"
  78. elif [[ -f "${SCRIPT_DIR}/.env.production.example" ]]; then
  79. $SUDO install -m 640 "${SCRIPT_DIR}/.env.production.example" "${ENV_FILE}"
  80. info "已从 .env.production.example 安装到 ${ENV_FILE}"
  81. else
  82. err "未找到 dataops.env 或 .env.production.example"
  83. exit 1
  84. fi
  85. $SUDO chown root:"${APP_GROUP}" "${ENV_FILE}" 2>/dev/null || true
  86. normalize_text_files "${ENV_FILE}"
  87. warn "首次部署:请编辑 ${ENV_FILE} 填入真实密钥后重新执行本脚本"
  88. exit 2
  89. }
  90. install_env_file
  91. if grep -qE 'replace-with|replace-n8n|your-api-key|your-deepseek' "${ENV_FILE}" 2>/dev/null; then
  92. warn "环境变量仍含模板占位符,请编辑 ${ENV_FILE} 后重新部署"
  93. warn "必须配置: SECRET_KEY, DEEPSEEK_API_KEY, N8N_API_KEY 及数据库/MinIO 连接"
  94. exit 2
  95. fi
  96. step "创建目录"
  97. $SUDO install -d -m 755 "${APP_DIR}" "${APP_DIR}/logs" /data/upload /data/archive /var/log/supervisor
  98. step "发布应用代码到 ${APP_DIR}"
  99. for target in app database; do
  100. $SUDO rm -rf "${APP_DIR}/${target}"
  101. $SUDO cp -a "${SCRIPT_DIR}/${target}" "${APP_DIR}/${target}"
  102. done
  103. for file in requirements.txt wsgi.py gunicorn_config.py __init__.py; do
  104. if [[ -f "${SCRIPT_DIR}/${file}" ]]; then
  105. $SUDO cp -f "${SCRIPT_DIR}/${file}" "${APP_DIR}/${file}"
  106. fi
  107. done
  108. step "发布运维脚本到 ${APP_DIR}/scripts"
  109. OPS_SRC="${SCRIPT_DIR}/scripts"
  110. if [[ ! -d "${OPS_SRC}" ]]; then
  111. err "缺少 scripts/ 目录"
  112. exit 1
  113. fi
  114. $SUDO rm -rf "${APP_DIR}/scripts"
  115. $SUDO cp -a "${OPS_SRC}" "${APP_DIR}/scripts"
  116. $SUDO chmod +x "${APP_DIR}/scripts/"*.sh
  117. $SUDO rm -f "${APP_DIR}/run_dataops.sh" 2>/dev/null || true
  118. normalize_text_files "${APP_DIR}/scripts"
  119. step "创建 Python 虚拟环境并安装依赖"
  120. if [[ ! -d "${APP_DIR}/venv" ]]; then
  121. $SUDO -u "${APP_USER}" python3 -m venv "${APP_DIR}/venv"
  122. fi
  123. $SUDO "${APP_DIR}/venv/bin/python" -m pip install --upgrade pip
  124. $SUDO "${APP_DIR}/venv/bin/pip" install -r "${APP_DIR}/requirements.txt"
  125. step "验证 Flask 应用"
  126. $SUDO -u "${APP_USER}" env APP_ENV_FILE="${ENV_FILE}" bash -c "
  127. set -a
  128. source <(sed 's/\r$//' '${ENV_FILE}' | sed '1s/^\xEF\xBB\xBF//')
  129. set +a
  130. cd '${APP_DIR}'
  131. '${APP_DIR}/venv/bin/python' -c \"from app import create_app; create_app(); print('Flask 应用验证通过')\"
  132. " || {
  133. err "应用验证失败,请检查 ${ENV_FILE}"
  134. exit 1
  135. }
  136. step "配置 Supervisor"
  137. if [[ -f "${APP_DIR}/gunicorn.conf.py" ]]; then
  138. $SUDO mv "${APP_DIR}/gunicorn.conf.py" "${APP_DIR}/gunicorn.conf.py.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
  139. warn "已备份旧 gunicorn.conf.py"
  140. fi
  141. $SUDO tee "/etc/supervisor/conf.d/${APP_NAME}.conf" >/dev/null <<EOF
  142. [program:${APP_NAME}]
  143. command=/usr/bin/env bash ${APP_DIR}/scripts/run_dataops.sh
  144. directory=${APP_DIR}
  145. user=${APP_USER}
  146. autostart=true
  147. autorestart=true
  148. startsecs=8
  149. startretries=5
  150. stopasgroup=true
  151. killasgroup=true
  152. redirect_stderr=true
  153. stdout_logfile=/var/log/supervisor/${APP_NAME}.log
  154. stdout_logfile_maxbytes=50MB
  155. stdout_logfile_backups=5
  156. environment=FLASK_ENV="production",APP_ENV_FILE="${ENV_FILE}",APP_DIR="${APP_DIR}",PATH="${APP_DIR}/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
  157. EOF
  158. step "设置权限"
  159. $SUDO chown -R "${APP_USER}:${APP_GROUP}" "${APP_DIR}" /data/upload /data/archive
  160. $SUDO chown root:"${APP_GROUP}" "${ENV_FILE}" 2>/dev/null || true
  161. $SUDO chmod 640 "${ENV_FILE}" 2>/dev/null || $SUDO chmod 600 "${ENV_FILE}"
  162. step "启动服务"
  163. $SUDO supervisorctl reread
  164. $SUDO supervisorctl update
  165. $SUDO supervisorctl restart "${APP_NAME}" 2>/dev/null || $SUDO supervisorctl start "${APP_NAME}"
  166. sleep 8
  167. health_ok=0
  168. for i in $(seq 1 8); do
  169. code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${LISTEN_PORT}/api/system/health" 2>/dev/null || echo 000)"
  170. if [[ "${code}" == "200" ]]; then
  171. health_ok=1
  172. break
  173. fi
  174. info "健康检查 ${i}/8: HTTP ${code},等待..."
  175. sleep 3
  176. done
  177. if [[ "${ENABLE_NGINX}" == "1" ]]; then
  178. require_command nginx
  179. step "配置 Nginx 反向代理"
  180. NGINX_CONF="/etc/nginx/sites-available/${APP_NAME}.conf"
  181. if [[ -f "${SCRIPT_DIR}/config/nginx-dataops-platform.conf" ]]; then
  182. $SUDO cp -f "${SCRIPT_DIR}/config/nginx-dataops-platform.conf" "${NGINX_CONF}"
  183. $SUDO sed -i "s/company.example.com/${NGINX_SERVER_NAME}/g" "${NGINX_CONF}"
  184. $SUDO sed -i "s/listen 18183/listen ${NGINX_LISTEN_PORT}/g" "${NGINX_CONF}"
  185. $SUDO sed -i "s/127.0.0.1:5500/127.0.0.1:${LISTEN_PORT}/g" "${NGINX_CONF}"
  186. else
  187. $SUDO tee "${NGINX_CONF}" >/dev/null <<EOF
  188. server {
  189. listen ${NGINX_LISTEN_PORT};
  190. server_name ${NGINX_SERVER_NAME};
  191. client_max_body_size 100m;
  192. location /api/bd/ddlparse {
  193. proxy_pass http://127.0.0.1:${LISTEN_PORT};
  194. proxy_http_version 1.1;
  195. proxy_set_header Host \$host;
  196. proxy_set_header X-Real-IP \$remote_addr;
  197. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  198. proxy_set_header X-Forwarded-Proto \$scheme;
  199. proxy_connect_timeout 60s;
  200. proxy_send_timeout 300s;
  201. proxy_read_timeout 300s;
  202. }
  203. location / {
  204. proxy_pass http://127.0.0.1:${LISTEN_PORT};
  205. proxy_http_version 1.1;
  206. proxy_set_header Host \$host;
  207. proxy_set_header X-Real-IP \$remote_addr;
  208. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  209. proxy_set_header X-Forwarded-Proto \$scheme;
  210. proxy_connect_timeout 60s;
  211. proxy_send_timeout 180s;
  212. proxy_read_timeout 180s;
  213. }
  214. }
  215. EOF
  216. fi
  217. $SUDO ln -sfn "${NGINX_CONF}" "/etc/nginx/sites-enabled/${APP_NAME}.conf"
  218. $SUDO nginx -t
  219. $SUDO systemctl reload nginx
  220. fi
  221. echo ""
  222. echo "=========================================="
  223. echo -e "${GREEN} 部署完成${NC}"
  224. echo "=========================================="
  225. echo " 应用目录: ${APP_DIR}"
  226. echo " 环境变量: ${ENV_FILE}"
  227. echo " 监听地址: ${LISTEN_HOST}:${LISTEN_PORT}"
  228. echo ""
  229. echo " 启动: sudo ${APP_DIR}/scripts/start_dataops.sh"
  230. echo " 停止: sudo ${APP_DIR}/scripts/stop_dataops.sh"
  231. echo " 重启: sudo ${APP_DIR}/scripts/restart_dataops.sh"
  232. echo " 健康: curl http://127.0.0.1:${LISTEN_PORT}/api/system/health"
  233. echo ""
  234. $SUDO supervisorctl status "${APP_NAME}" || true
  235. if [[ "${health_ok}" -eq 1 ]]; then
  236. info "健康检查通过"
  237. else
  238. warn "健康检查未通过,请查看: tail -f /var/log/supervisor/${APP_NAME}.log"
  239. exit 1
  240. fi