e20e235a1e
scripts/deploy.sh covers the full path from fresh OS to running service: 1. install system packages (apt/yum/dnf auto-detect, includes bind/python3/venv/git) 2. create Python venv + pip install -r requirements.txt (avoids PEP 668) 3. init SQLite DB (default admin/admin) 4. delegate BIND config to setup-bind.sh (or --skip-bind) 5. install + enable systemd unit dns-web (gunicorn, port 5300) 6. restart + verify (port listen + HTTP 302) Idempotent; supports --dry-run, --skip-bind, --port, DNS_WEB_PORT/USER/WORKERS. Type=exec (not Type=notify) because gunicorn 21.x has no sd_notify. README updated with deploy.sh as recommended path; .gitignore adds venv/.
275 lines
10 KiB
Bash
275 lines
10 KiB
Bash
#!/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=<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 "$@"
|