#!/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 "$@"