diff --git a/.gitignore b/.gitignore index a6fbaab..4284aaa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ instance/ *.bak .env .venv/ +venv/ diff --git a/README.md b/README.md index 454e78b..f749659 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,51 @@ dns-service/ ## 安装部署 -### 1. 安装 BIND9 +### 方式一:一键部署(推荐) + +`scripts/deploy.sh` 从干净系统一路到运行中的服务,自动完成: + +1. 安装系统包(bind9 / bind-utils / python3 / pip / venv / git,自动识别 apt/yum/dnf) +2. 创建 Python 虚拟环境 `venv/`,安装 `requirements.txt`(用 venv 的 pip,避开 Debian 12+ PEP 668) +3. 初始化 SQLite 数据库(默认 `admin/admin`) +4. 调用 `scripts/setup-bind.sh` 配置 BIND(zone 目录、named.conf.local include、权限) +5. 安装并启用 systemd 服务 `dns-web`(gunicorn,端口 5300,2 workers) +6. 启动服务并验证端口 + HTTP 响应 + +```bash +git clone ssh://git@git.cnbugs.com:10022/AI-Agent/dns-service.git +cd dns-service + +# 先预览会做什么(推荐): +sudo bash scripts/deploy.sh --dry-run + +# 确认后真跑: +sudo bash scripts/deploy.sh + +# 如果 BIND 已经配好了只想更新应用: +sudo bash scripts/deploy.sh --skip-bind + +# 换端口: +sudo bash scripts/deploy.sh --port 5301 +# 或用环境变量: +sudo DNS_WEB_PORT=5301 DNS_WEB_WORKERS=4 bash scripts/deploy.sh +``` + +脚本幂等,可重复执行。升级代码后重跑一次即可(venv 和 DB 保留,只补缺失部分)。 + +**可调环境变量:** + +| 变量 | 默认 | 说明 | +|------|------|------| +| `DNS_WEB_PORT` | `5300` | 监听端口 | +| `DNS_WEB_USER` | `root` | 服务运行用户(需能操作 BIND 配置 + systemctl) | +| `DNS_WEB_WORKERS` | `2` | gunicorn worker 数 | + +> **关于 root 用户**:Web 应用需要 root 权限来操作 `/etc/bind/*` 配置文件和执行 `systemctl`/`rndc` 命令。如果你的环境不允许 root 跑服务,用 `DNS_WEB_USER=` 覆盖,并给该用户配 sudo 规则。systemd unit 用 `Type=exec`(gunicorn 21.x 不带 sd_notify,用 `Type=notify` 会卡 90 秒超时然后失败)。 + +### 方式二:手动部署 + +适合需要完全控制每一步的场景。 ```bash apt-get update diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..fcae612 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,274 @@ +#!/usr/bin/env bash +# deploy.sh - one-click deployment for DNS Web Manager +# +# From a fresh OS to a running service in one shot: +# 1. Install system packages (BIND, Python, pip, venv, git) +# 2. Create Python venv and install requirements.txt +# 3. Initialize SQLite database (default admin/admin) +# 4. Configure BIND (delegates to scripts/setup-bind.sh) +# 5. Install + enable systemd service (gunicorn, port 5300) +# 6. (Re)start the service and verify +# +# Usage: +# sudo bash scripts/deploy.sh # full deploy +# sudo bash scripts/deploy.sh --dry-run # preview, no changes +# sudo bash scripts/deploy.sh --skip-bind # skip BIND setup if already done +# sudo bash scripts/deploy.sh --port 5300 # override listen port +# +# Idempotent: safe to run multiple times. Existing venv/DB/config are +# preserved; only missing pieces are created. +# +# Tunables (env vars): +# DNS_WEB_PORT - listen port (default 5300) +# DNS_WEB_USER - service user (default root, needed for BIND config) +# DNS_WEB_WORKERS - gunicorn worker count (default 2) + +set -euo pipefail + +# ─── parse args ──────────────────────────────────────────────────────────── +DRY_RUN=0 +SKIP_BIND=0 +SERVICE_PORT="${DNS_WEB_PORT:-5300}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --skip-bind) SKIP_BIND=1; shift ;; + --port) SERVICE_PORT="$2"; shift 2 ;; + -h|--help) sed -n '2,22p' "$0"; exit 0 ;; + *) + echo "Unknown argument: $1" >&2 + exit 1 + ;; + esac +done + +# ─── resolve project root from script location ───────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_DIR" + +SERVICE_NAME="dns-web" +SERVICE_USER="${DNS_WEB_USER:-root}" +WORKERS="${DNS_WEB_WORKERS:-2}" +VENV_DIR="$PROJECT_DIR/venv" + +# ─── helpers ─────────────────────────────────────────────────────────────── +log() { printf '\033[1;34m[deploy]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[deploy]\033[0m %s\n' "$*" >&2; } +err() { printf '\033[1;31m[deploy]\033[0m %s\n' "$*" >&2; } + +run() { + if [[ $DRY_RUN -eq 1 ]]; then + printf ' [DRY-RUN] %s\n' "$*" + else + "$@" + fi +} + +must_be_root() { + if [[ $EUID -ne 0 ]]; then + err "Must run as root (use sudo)" + exit 1 + fi +} + +# ─── detect distro via package-manager presence (not os-release) ────────── +# File-based detection for BIND layout is in setup-bind.sh; here we only need +# the package manager. Check command -v first, fall back to os-release. +detect_distro() { + if command -v apt-get >/dev/null 2>&1; then + DISTRO="debian" + PKG_INSTALL=(apt-get install -y) + SYS_PKGS=(bind9 bind9utils dnsutils python3 python3-pip python3-venv git) + elif command -v dnf >/dev/null 2>&1; then + DISTRO="rhel" + PKG_INSTALL=(dnf install -y) + SYS_PKGS=(bind bind-utils python3 python3-pip python3-virtualenv git) + elif command -v yum >/dev/null 2>&1; then + DISTRO="rhel" + PKG_INSTALL=(yum install -y) + SYS_PKGS=(bind bind-utils python3 python3-pip python3-virtualenv git) + else + err "Unsupported distro: no apt-get/yum/dnf found" + exit 1 + fi + log "Detected distro: $DISTRO (pkg mgr: ${PKG_INSTALL[0]})" +} + +# ─── steps ──────────────────────────────────────────────────────────────── +install_system_deps() { + log "Step 1: install system packages" + log " packages: ${SYS_PKGS[*]}" + if [[ "$DISTRO" == "debian" ]]; then + # apt-get needs a refresh of the package index first; yum/dnf don't + run apt-get update -qq + fi + run "${PKG_INSTALL[@]}" "${SYS_PKGS[@]}" +} + +setup_venv() { + log "Step 2: set up Python venv" + if [[ ! -d "$VENV_DIR" ]]; then + log " creating venv at $VENV_DIR" + run python3 -m venv "$VENV_DIR" + else + log " venv already exists, reusing" + fi + log " upgrading pip" + run "$VENV_DIR/bin/pip" install --upgrade pip --quiet + log " installing requirements.txt" + run "$VENV_DIR/bin/pip" install -r "$PROJECT_DIR/requirements.txt" --quiet +} + +init_database() { + log "Step 3: initialize SQLite database" + run mkdir -p "$PROJECT_DIR/instance" + run "$VENV_DIR/bin/python" "$PROJECT_DIR/init_db.py" +} + +configure_bind() { + log "Step 4: configure BIND (delegating to setup-bind.sh)" + if [[ ! -f "$SCRIPT_DIR/setup-bind.sh" ]]; then + warn " $SCRIPT_DIR/setup-bind.sh not found; skipping BIND setup" + return + fi + if [[ $DRY_RUN -eq 1 ]]; then + printf ' [DRY-RUN] bash %s/setup-bind.sh\n' "$SCRIPT_DIR" + else + bash "$SCRIPT_DIR/setup-bind.sh" + fi +} + +install_systemd_unit() { + log "Step 5: install systemd service ($SERVICE_NAME on port $SERVICE_PORT)" + local unit_file="/etc/systemd/system/${SERVICE_NAME}.service" + local gunicorn_bin="$VENV_DIR/bin/gunicorn" + + # Note on Type=exec vs Type=notify: gunicorn 21.x does NOT ship sd_notify + # support out of the box. Type=notify would hang for DefaultTimeoutStartSec + # (90s) then mark the unit failed. Type=exec considers the unit "running" + # as soon as the exec() succeeds, which is what we want. + local unit_content + unit_content="[Unit] +Description=DNS Web Manager (BIND9 web UI) +Documentation=https://git.cnbugs.com/AI-Agent/dns-service +After=network.target named.service +Wants=named.service + +[Service] +Type=exec +User=$SERVICE_USER +WorkingDirectory=$PROJECT_DIR +ExecStart=$gunicorn_bin --workers $WORKERS --bind 0.0.0.0:$SERVICE_PORT wsgi:app +Restart=always +RestartSec=5 +# Optional: override BIND paths via env vars (see README \"路径优先级\") +# Environment=BIND_ZONES_DIR=/opt/bind/zones + +[Install] +WantedBy=multi-user.target +" + + if [[ $DRY_RUN -eq 1 ]]; then + printf ' [DRY-RUN] write %s\n' "$unit_file" + printf '%s\n' "$unit_content" | sed 's/^/ | /' + else + printf '%s\n' "$unit_content" > "$unit_file" + chmod 644 "$unit_file" + log " wrote $unit_file" + systemctl daemon-reload + systemctl enable "$SERVICE_NAME" 2>&1 | grep -v 'Created symlink' || true + log " enabled $SERVICE_NAME" + fi +} + +restart_service() { + log "Step 6: (re)start $SERVICE_NAME" + if [[ $DRY_RUN -eq 1 ]]; then + printf ' [DRY-RUN] systemctl restart %s\n' "$SERVICE_NAME" + return + fi + systemctl restart "$SERVICE_NAME" + sleep 2 + if systemctl is-active --quiet "$SERVICE_NAME"; then + log " $SERVICE_NAME is active" + else + err " $SERVICE_NAME failed to start" + err " check: journalctl -xe -u $SERVICE_NAME -n 30" + return 1 + fi +} + +verify() { + log "Step 7: verify deployment" + if [[ $DRY_RUN -eq 1 ]]; then + return + fi + # Gunicorn needs a moment to bind the port after exec(); retry a few + # times so we don't false-alarm "not listening" on a slow box. + local port_ok=0 http_code + for _ in 1 2 3 4 5; do + if ss -lnt 2>/dev/null | grep -qE ":$SERVICE_PORT\b"; then + port_ok=1 + break + fi + sleep 1 + done + if [[ $port_ok -eq 1 ]]; then + log " port $SERVICE_PORT is listening" + else + warn " port $SERVICE_PORT not listening after 5s (may still be warming up)" + fi + # HTTP responding? 302 = login redirect (expected), 200 = some page. + http_code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$SERVICE_PORT/" --max-time 5 || echo "000") + if [[ "$http_code" == "302" || "$http_code" == "200" ]]; then + log " HTTP check: $http_code (302=login redirect, 200=page)" + else + warn " HTTP check returned $http_code (expected 302/200)" + warn " check: journalctl -u $SERVICE_NAME -n 30" + fi +} + +print_summary() { + log "════════════════════════════════════════════════════════" + log " Deploy complete" + log "════════════════════════════════════════════════════════" + log " Service : systemctl status $SERVICE_NAME" + log " Logs : journalctl -u $SERVICE_NAME -f" + log " URL : http://$(hostname -I 2>/dev/null | awk '{print $1}'):${SERVICE_PORT}" + log " Login : admin / admin (change after first login!)" + log " Reload : bash $SCRIPT_DIR/deploy.sh" + if [[ "$SERVICE_USER" == "root" ]]; then + warn " Runs as root (required to manage BIND config + systemctl)." + warn " If your env blocks root services, override with DNS_WEB_USER=" + warn " and grant that user sudo on systemctl/named/chown instead." + fi + log "════════════════════════════════════════════════════════" +} + +# ─── main ───────────────────────────────────────────────────────────────── +main() { + must_be_root + detect_distro + if [[ $DRY_RUN -eq 1 ]]; then + log "DRY-RUN mode: no changes will be made" + log " project dir : $PROJECT_DIR" + log " venv : $VENV_DIR" + log " service : $SERVICE_NAME ($SERVICE_USER@$SERVICE_PORT, $WORKERS workers)" + fi + install_system_deps + setup_venv + init_database + if [[ $SKIP_BIND -eq 0 ]]; then + configure_bind + else + warn "Skipping BIND setup (--skip-bind)" + fi + install_systemd_unit + restart_service + verify + print_summary +} + +main "$@"