deploy_dataops.sh 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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. DATABASE_ENV_VALIDATOR="${SCRIPT_DIR}/app/config/database_urls.py"
  92. if [[ ! -f "${DATABASE_ENV_VALIDATOR}" ]] || \
  93. ! python3 "${DATABASE_ENV_VALIDATOR}" --env-file "${ENV_FILE}"; then
  94. warn "数据库连接或 runtime 密码仍为空、格式错误或包含模板占位值"
  95. exit 2
  96. fi
  97. if grep -qE 'replace-with|replace-n8n|your-api-key|your-deepseek' "${ENV_FILE}" 2>/dev/null; then
  98. warn "环境变量仍含模板占位符,请编辑 ${ENV_FILE} 后重新部署"
  99. warn "必须配置: SECRET_KEY, DEEPSEEK_API_KEY, N8N_API_KEY 及数据库/MinIO 连接"
  100. exit 2
  101. fi
  102. step "创建目录"
  103. $SUDO install -d -m 755 "${APP_DIR}" "${APP_DIR}/logs" /data/upload /data/archive /var/log/supervisor
  104. step "发布应用代码到 ${APP_DIR}"
  105. for target in app database; do
  106. $SUDO rm -rf "${APP_DIR}/${target}"
  107. $SUDO cp -a "${SCRIPT_DIR}/${target}" "${APP_DIR}/${target}"
  108. done
  109. for file in requirements.txt wsgi.py gunicorn_config.py __init__.py; do
  110. if [[ -f "${SCRIPT_DIR}/${file}" ]]; then
  111. $SUDO cp -f "${SCRIPT_DIR}/${file}" "${APP_DIR}/${file}"
  112. fi
  113. done
  114. step "发布运维脚本到 ${APP_DIR}/scripts"
  115. OPS_SRC="${SCRIPT_DIR}/scripts"
  116. if [[ ! -d "${OPS_SRC}" ]]; then
  117. err "缺少 scripts/ 目录"
  118. exit 1
  119. fi
  120. $SUDO rm -rf "${APP_DIR}/scripts"
  121. $SUDO cp -a "${OPS_SRC}" "${APP_DIR}/scripts"
  122. $SUDO chmod +x "${APP_DIR}/scripts/"*.sh
  123. $SUDO rm -f "${APP_DIR}/run_dataops.sh" 2>/dev/null || true
  124. normalize_text_files "${APP_DIR}/scripts"
  125. step "创建 Python 虚拟环境并安装依赖"
  126. if [[ ! -d "${APP_DIR}/venv" ]]; then
  127. $SUDO -u "${APP_USER}" python3 -m venv "${APP_DIR}/venv"
  128. fi
  129. $SUDO "${APP_DIR}/venv/bin/python" -m pip install --upgrade pip
  130. $SUDO "${APP_DIR}/venv/bin/pip" install -r "${APP_DIR}/requirements.txt"
  131. step "验证 Flask 应用"
  132. $SUDO -u "${APP_USER}" env APP_ENV_FILE="${ENV_FILE}" bash -c "
  133. set -a
  134. source <(sed 's/\r$//' '${ENV_FILE}' | sed '1s/^\xEF\xBB\xBF//')
  135. set +a
  136. cd '${APP_DIR}'
  137. '${APP_DIR}/venv/bin/python' -c \"from app import create_app; create_app(); print('Flask 应用验证通过')\"
  138. " || {
  139. err "应用验证失败,请检查 ${ENV_FILE}"
  140. exit 1
  141. }
  142. step "配置 Supervisor"
  143. if [[ -f "${APP_DIR}/gunicorn.conf.py" ]]; then
  144. $SUDO mv "${APP_DIR}/gunicorn.conf.py" "${APP_DIR}/gunicorn.conf.py.bak.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
  145. warn "已备份旧 gunicorn.conf.py"
  146. fi
  147. $SUDO tee "/etc/supervisor/conf.d/${APP_NAME}.conf" >/dev/null <<EOF
  148. [program:${APP_NAME}]
  149. command=/usr/bin/env bash ${APP_DIR}/scripts/run_dataops.sh
  150. directory=${APP_DIR}
  151. user=${APP_USER}
  152. autostart=true
  153. autorestart=true
  154. startsecs=8
  155. startretries=5
  156. stopasgroup=true
  157. killasgroup=true
  158. redirect_stderr=true
  159. stdout_logfile=/var/log/supervisor/${APP_NAME}.log
  160. stdout_logfile_maxbytes=50MB
  161. stdout_logfile_backups=5
  162. 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"
  163. EOF
  164. step "设置权限"
  165. $SUDO chown -R "${APP_USER}:${APP_GROUP}" "${APP_DIR}" /data/upload /data/archive
  166. $SUDO chown root:"${APP_GROUP}" "${ENV_FILE}" 2>/dev/null || true
  167. $SUDO chmod 640 "${ENV_FILE}" 2>/dev/null || $SUDO chmod 600 "${ENV_FILE}"
  168. step "启动服务"
  169. $SUDO supervisorctl reread
  170. $SUDO supervisorctl update
  171. $SUDO supervisorctl restart "${APP_NAME}" 2>/dev/null || $SUDO supervisorctl start "${APP_NAME}"
  172. sleep 8
  173. health_ok=0
  174. for i in $(seq 1 8); do
  175. code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${LISTEN_PORT}/api/system/health" 2>/dev/null || echo 000)"
  176. if [[ "${code}" == "200" ]]; then
  177. health_ok=1
  178. break
  179. fi
  180. info "健康检查 ${i}/8: HTTP ${code},等待..."
  181. sleep 3
  182. done
  183. if [[ "${ENABLE_NGINX}" == "1" ]]; then
  184. require_command nginx
  185. step "配置 Nginx 反向代理"
  186. NGINX_CONF="/etc/nginx/sites-available/${APP_NAME}.conf"
  187. if [[ -f "${SCRIPT_DIR}/config/nginx-dataops-platform.conf" ]]; then
  188. $SUDO cp -f "${SCRIPT_DIR}/config/nginx-dataops-platform.conf" "${NGINX_CONF}"
  189. $SUDO sed -i "s/company.example.com/${NGINX_SERVER_NAME}/g" "${NGINX_CONF}"
  190. $SUDO sed -i "s/listen 18183/listen ${NGINX_LISTEN_PORT}/g" "${NGINX_CONF}"
  191. $SUDO sed -i "s/127.0.0.1:5500/127.0.0.1:${LISTEN_PORT}/g" "${NGINX_CONF}"
  192. else
  193. $SUDO tee "${NGINX_CONF}" >/dev/null <<EOF
  194. server {
  195. listen ${NGINX_LISTEN_PORT};
  196. server_name ${NGINX_SERVER_NAME};
  197. client_max_body_size 100m;
  198. location /api/bd/ddlparse {
  199. proxy_pass http://127.0.0.1:${LISTEN_PORT};
  200. proxy_http_version 1.1;
  201. proxy_set_header Host \$host;
  202. proxy_set_header X-Real-IP \$remote_addr;
  203. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  204. proxy_set_header X-Forwarded-Proto \$scheme;
  205. proxy_connect_timeout 60s;
  206. proxy_send_timeout 300s;
  207. proxy_read_timeout 300s;
  208. }
  209. location / {
  210. proxy_pass http://127.0.0.1:${LISTEN_PORT};
  211. proxy_http_version 1.1;
  212. proxy_set_header Host \$host;
  213. proxy_set_header X-Real-IP \$remote_addr;
  214. proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
  215. proxy_set_header X-Forwarded-Proto \$scheme;
  216. proxy_connect_timeout 60s;
  217. proxy_send_timeout 180s;
  218. proxy_read_timeout 180s;
  219. }
  220. }
  221. EOF
  222. fi
  223. $SUDO ln -sfn "${NGINX_CONF}" "/etc/nginx/sites-enabled/${APP_NAME}.conf"
  224. $SUDO nginx -t
  225. $SUDO systemctl reload nginx
  226. fi
  227. echo ""
  228. echo "=========================================="
  229. echo -e "${GREEN} 部署完成${NC}"
  230. echo "=========================================="
  231. echo " 应用目录: ${APP_DIR}"
  232. echo " 环境变量: ${ENV_FILE}"
  233. echo " 监听地址: ${LISTEN_HOST}:${LISTEN_PORT}"
  234. echo ""
  235. echo " 启动: sudo ${APP_DIR}/scripts/start_dataops.sh"
  236. echo " 停止: sudo ${APP_DIR}/scripts/stop_dataops.sh"
  237. echo " 重启: sudo ${APP_DIR}/scripts/restart_dataops.sh"
  238. echo " 健康: curl http://127.0.0.1:${LISTEN_PORT}/api/system/health"
  239. echo ""
  240. $SUDO supervisorctl status "${APP_NAME}" || true
  241. if [[ "${health_ok}" -eq 1 ]]; then
  242. info "健康检查通过"
  243. else
  244. warn "健康检查未通过,请查看: tail -f /var/log/supervisor/${APP_NAME}.log"
  245. exit 1
  246. fi