From aebae3b604610564a4ce956c34b26c8c8b564da0 Mon Sep 17 00:00:00 2001 From: MohammadAmin Baranzehi Date: Mon, 14 Sep 2026 13:55:14 +0330 Subject: [PATCH] feat(cli): introduce hdg global CLI tool with self-install, self-update and shorthand syntax - hdg: Global executable with auto-symlinking to /usr/local/bin, self-update, and auto-setup - cli.py: Unified Python CLI with setup, repair, doctor, update, and install commands - add-gunicorn-service.sh: Kept as backward-compatible wrapper redirecting to hdg --- add-gunicorn-service.sh | 96 +--------- cli.py | 391 ++++++++++++++++++++++++++++++++++++++++ hdg | 342 +++++++++++++++++++++++++++++++++++ 3 files changed, 738 insertions(+), 91 deletions(-) create mode 100644 cli.py create mode 100644 hdg diff --git a/add-gunicorn-service.sh b/add-gunicorn-service.sh index 1cda828..c37752a 100644 --- a/add-gunicorn-service.sh +++ b/add-gunicorn-service.sh @@ -1,92 +1,6 @@ #!/usr/bin/env bash - -# File: add-gunicorn-service.sh -# Usage: ./add-gunicorn-service.sh domain.com hestiauser projectname - -set -e - -if [ "$#" -ne 3 ]; then - echo "Usage: $0 domain hestia_user project_name" - echo "Example: $0 example.com admin myproject" - exit 1 -fi - -DOMAIN="$1" -USER="$2" -PROJECT="$3" - -HOMEDIR="/home/$USER" -WEBDIR="$HOMEDIR/web/$DOMAIN/public_html" -VENV_BIN="$WEBDIR/venv/bin" -SOCK_PATH="$WEBDIR/gunicorn.sock" -SERVICE_NAME="gunicorn-$DOMAIN" - -SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service" - -if [ ! -d "$WEBDIR" ]; then - echo "Error: Domain directory not found → $WEBDIR" - exit 1 -fi - -if [ ! -d "$VENV_BIN" ]; then - echo "Warning: Virtualenv not found → $VENV_BIN" - echo "Assuming gunicorn is installed inside the venv anyway" -fi - -echo "Creating service for: $DOMAIN" -echo "User: $USER" -echo "Project: $PROJECT" -echo "Socket path: $SOCK_PATH" -echo "Service name: $SERVICE_NAME" -echo "" - -cat << EOF | sudo tee "$SERVICE_FILE" > /dev/null -[Unit] -Description=Gunicorn instance for $DOMAIN (Django) -After=network.target - -[Service] -User=$USER -Group=www-data -WorkingDirectory=$WEBDIR -Environment="PATH=$VENV_BIN:/usr/local/bin:/usr/bin" -Environment="PYTHONUNBUFFERED=1" -ExecStart=$VENV_BIN/gunicorn \\ - --access-logfile - \\ - --workers 3 \\ - --threads 2 \\ - --worker-class=gthread \\ - --bind unix:$SOCK_PATH \\ - --log-level=info \\ - $PROJECT.wsgi:application - -Restart=always -RestartSec=3 - -[Install] -WantedBy=multi-user.target -EOF - -echo "Service file created: $SERVICE_FILE" - -sudo systemctl daemon-reload -echo "→ daemon-reload completed" - -sudo systemctl stop "$SERVICE_NAME" 2>/dev/null || true - -sudo systemctl enable "$SERVICE_NAME" -sudo systemctl start "$SERVICE_NAME" - -echo "" -echo "Service status:" -sudo systemctl status "$SERVICE_NAME" --no-pager -l | head -n 12 - -echo "" -echo "Socket check:" -ls -l "$SOCK_PATH" 2>/dev/null || echo "Socket not yet created (service must be running)" - -echo "" -echo "If you get 502 Bad Gateway:" -echo "1. Make sure gunicorn is installed in venv: $VENV_BIN/pip install gunicorn" -echo "2. Run migrate & collectstatic if needed" -echo "3. Restart service: sudo systemctl restart $SERVICE_NAME" \ No newline at end of file +# ────────────────────────────────────────────────────────────────────────── +# Backward-compatibility wrapper -> redirects to 'hdg' +# ────────────────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "$SCRIPT_DIR/hdg" "$@" \ No newline at end of file diff --git a/cli.py b/cli.py new file mode 100644 index 0000000..07539ad --- /dev/null +++ b/cli.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +HestiaCP Django-Gunicorn Deployment CLI. + +A clean, modular command-line interface that orchestrates all deployment, +repair, and diagnostic operations. + +Usage: + python3 cli.py setup [project] + python3 cli.py repair + python3 cli.py doctor + python3 cli.py install-templates + python3 cli.py status +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +# Ensure the project root is on sys.path so ``core`` can be imported +# regardless of where the script is invoked from. +PROJECT_ROOT = Path(__file__).resolve().parent +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from core.detector import DjangoProjectDetector +from core.doctor import DeploymentDoctor, print_report +from core.executor import LinuxExecutor +from core.migrator import ProjectMigrator +from core.models import DomainConfig, ServiceConfig +from core.nginx_manager import HestiaNginxManager +from core.service_generator import SystemdServiceGenerator + + +# ────────────────────────────────────────────────────────────────────────── +# ANSI helpers +# ────────────────────────────────────────────────────────────────────────── +RESET = "\033[0m" +GREEN = "\033[92m" +YELLOW = "\033[93m" +RED = "\033[91m" +BOLD = "\033[1m" +CYAN = "\033[96m" + + +def info(msg: str) -> None: + print(f" {CYAN}→{RESET} {msg}") + + +def success(msg: str) -> None: + print(f" {GREEN}✔{RESET} {msg}") + + +def warn(msg: str) -> None: + print(f" {YELLOW}⚠{RESET} {msg}") + + +def error(msg: str) -> None: + print(f" {RED}✘{RESET} {msg}") + + +def banner() -> None: + print(f""" +{BOLD}┌──────────────────────────────────────────────────────────┐ +│ HestiaCP + Django + Gunicorn — Deployment Engine │ +│ Prepared by MohammadAmin Baranzehi (amin.baranzehi.com) │ +└──────────────────────────────────────────────────────────┘{RESET} +""") + + +# ────────────────────────────────────────────────────────────────────────── +# Shared factory +# ────────────────────────────────────────────────────────────────────────── +def _build_components(): + """Instantiate shared components with dependency injection.""" + executor = LinuxExecutor() + detector = DjangoProjectDetector() + generator = SystemdServiceGenerator() + nginx_mgr = HestiaNginxManager(executor) + migrator = ProjectMigrator(executor, detector, generator) + doctor = DeploymentDoctor(executor) + return executor, detector, generator, nginx_mgr, migrator, doctor + + +# ────────────────────────────────────────────────────────────────────────── +# Sub-command handlers +# ────────────────────────────────────────────────────────────────────────── +def cmd_setup(args: argparse.Namespace) -> int: + """Deploy a new Django project on HestiaCP.""" + banner() + executor, detector, generator, nginx_mgr, _, doctor = _build_components() + + dc = DomainConfig(domain=args.domain, user=args.user) + info(f"Domain : {dc.domain}") + info(f"User : {dc.user}") + info(f"Web dir: {dc.web_dir}") + print() + + # 1. Detect project + info("Detecting Django project structure...") + project_info = detector.detect(dc.web_dir, project_hint=args.project) + + if not project_info.is_valid: + error("Project detection failed:") + for err in project_info.errors: + error(f" {err}") + return 1 + + success(f"Layout : {project_info.layout.name}") + success(f"WSGI : {project_info.wsgi_module}") + success(f"Venv : {project_info.venv_path or 'NOT FOUND'}") + success(f"Static : {project_info.static_root_name}") + success(f"DB : {project_info.database_engine.name}") + if project_info.has_dot_env: + success(".env file detected — will load via EnvironmentFile") + print() + + # 2. Install templates if not present + if not nginx_mgr.is_template_installed(): + info("Installing NGINX templates...") + tpl_dir = PROJECT_ROOT / "templates" + nginx_mgr.install_templates( + tpl_dir / "django-gunicorn.tpl", + tpl_dir / "django-gunicorn.stpl", + ) + success("NGINX templates installed.") + else: + success("NGINX templates already installed.") + + # 3. Generate service file + info("Generating systemd service...") + service_config = ServiceConfig(domain_config=dc, project_info=project_info) + content = generator.generate(service_config) + + # Backup if exists + if executor.file_exists(dc.service_file): + bak = executor.backup_file(dc.service_file) + warn(f"Existing service backed up to: {bak}") + + executor.write_file(dc.service_file, content, mode=0o644) + success(f"Service file: {dc.service_file}") + success(f"Workers: {service_config.workers} | Threads: {service_config.threads} | umask: {service_config.umask}") + print() + + # 4. Enable and start + info("Starting Gunicorn service...") + executor.run("systemctl daemon-reload") + executor.run(f"systemctl enable {dc.service_name}", check=False) + executor.run(f"systemctl restart {dc.service_name}", check=False) + + # 5. Rebuild NGINX + info("Rebuilding NGINX for domain...") + nginx_mgr.rebuild_domain(dc) + print() + + # 6. Fix permissions + info("Fixing file permissions...") + db_file = dc.web_dir / "db.sqlite3" + if executor.file_exists(db_file): + executor.run(f"chown {dc.user}:www-data {db_file} && chmod 664 {db_file}", check=False) + success("db.sqlite3 permissions fixed.") + + static_dir = dc.web_dir / project_info.static_root_name + if executor.file_exists(static_dir): + executor.run(f"chown -R {dc.user}:www-data {static_dir}", check=False) + success(f"{project_info.static_root_name}/ ownership fixed.") + print() + + # 7. Run doctor + info("Running health checks...") + report = doctor.diagnose(dc) + print_report(report) + + return 0 if report.is_healthy else 1 + + +def cmd_repair(args: argparse.Namespace) -> int: + """Repair / upgrade a previously deployed project.""" + banner() + executor, detector, generator, nginx_mgr, migrator, _ = _build_components() + + dc = DomainConfig(domain=args.domain, user=args.user) + info(f"Repairing deployment for: {dc.domain}") + print() + + if not migrator.needs_migration(dc): + success("No migration needed — deployment looks up-to-date.") + return 0 + + # Update templates first + info("Updating NGINX templates...") + tpl_dir = PROJECT_ROOT / "templates" + nginx_mgr.install_templates( + tpl_dir / "django-gunicorn.tpl", + tpl_dir / "django-gunicorn.stpl", + ) + success("NGINX templates updated.") + + # Run migration + info("Migrating service configuration...") + report = migrator.migrate(dc) + + # Rebuild NGINX + info("Rebuilding NGINX configuration...") + nginx_mgr.rebuild_domain(dc) + + print_report(report) + return 0 if report.is_healthy else 1 + + +def cmd_doctor(args: argparse.Namespace) -> int: + """Run diagnostics on a deployment.""" + banner() + _, _, _, _, _, doctor = _build_components() + + dc = DomainConfig(domain=args.domain, user=args.user) + report = doctor.diagnose(dc) + print_report(report) + + return 0 if report.is_healthy else 1 + + +def cmd_install_templates(args: argparse.Namespace) -> int: + """Install NGINX templates to HestiaCP.""" + banner() + executor, _, _, nginx_mgr, _, _ = _build_components() + + tpl_dir = PROJECT_ROOT / "templates" + info("Installing NGINX templates...") + nginx_mgr.install_templates( + tpl_dir / "django-gunicorn.tpl", + tpl_dir / "django-gunicorn.stpl", + ) + success("Templates installed to HestiaCP successfully.") + return 0 + + +def cmd_status(args: argparse.Namespace) -> int: + """Show status of a Gunicorn service.""" + banner() + executor, _, _, _, _, _ = _build_components() + + service_name = f"gunicorn-{args.domain}" + rc, stdout, stderr = executor.run( + f"systemctl status {service_name} --no-pager -l", + check=False, + ) + print(stdout) + if stderr: + print(stderr) + return rc + + +def cmd_update(args: argparse.Namespace) -> int: + """Self-update the deployment engine and refresh templates.""" + banner() + executor, _, _, nginx_mgr, _, _ = _build_components() + + info("Updating HestiaCP Django-Gunicorn Deployment Engine...") + + # 1. Pull latest from git if repository + git_dir = PROJECT_ROOT / ".git" + if git_dir.exists(): + info("Pulling latest updates from git...") + rc, out, err = executor.run(f"git -C {PROJECT_ROOT} pull --ff-only", check=False) + if rc == 0: + success("Repository updated to latest commit.") + else: + rc2, out2, err2 = executor.run(f"git -C {PROJECT_ROOT} pull", check=False) + if rc2 == 0: + success("Repository updated.") + else: + warn(f"Could not pull updates from git: {err or err2}") + + # 2. Update templates in HestiaCP + tpl_dir = PROJECT_ROOT / "templates" + info("Updating NGINX templates...") + nginx_mgr.install_templates( + tpl_dir / "django-gunicorn.tpl", + tpl_dir / "django-gunicorn.stpl", + ) + success("NGINX templates updated in HestiaCP.") + + # 3. Ensure global symlink + cmd_install(args) + + success("Update completed successfully! 'hdg' is up to date.") + return 0 + + +def cmd_install(args: argparse.Namespace) -> int: + """Install hdg globally to /usr/local/bin.""" + executor, _, _, nginx_mgr, _, _ = _build_components() + bin_dir = Path("/usr/local/bin") + hdg_script = PROJECT_ROOT / "hdg" + + if bin_dir.exists() and hdg_script.exists(): + for link_name in ["hdg", "v-django", "hestia-django"]: + target = bin_dir / link_name + executor.run(f"ln -sf {hdg_script} {target}", check=False) + executor.run(f"chmod +x {hdg_script}", check=False) + success("Global commands installed: 'hdg', 'v-django', 'hestia-django'") + return 0 + + +# ────────────────────────────────────────────────────────────────────────── +# Argument parser +# ────────────────────────────────────────────────────────────────────────── +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="hdg", + description="HestiaCP + Django + Gunicorn Deployment Engine (hdg)", + ) + sub = parser.add_subparsers(dest="command", help="Available commands") + + # setup + p_setup = sub.add_parser("setup", help="Deploy a new Django project") + p_setup.add_argument("domain", help="Domain name (e.g. example.com)") + p_setup.add_argument("user", help="HestiaCP system user") + p_setup.add_argument("project", nargs="?", default=None, + help="Project name (auto-detected if omitted)") + + # repair + p_repair = sub.add_parser("repair", help="Repair a previously deployed project") + p_repair.add_argument("domain", help="Domain name") + p_repair.add_argument("user", help="HestiaCP system user") + + # doctor + p_doctor = sub.add_parser("doctor", help="Run deployment health checks") + p_doctor.add_argument("domain", help="Domain name") + p_doctor.add_argument("user", help="HestiaCP system user") + + # install-templates + sub.add_parser("install-templates", help="Install NGINX templates to HestiaCP") + + # update + sub.add_parser("update", help="Update hdg and NGINX templates to latest version") + + # install + sub.add_parser("install", help="Install global symlinks to /usr/local/bin") + + # status + p_status = sub.add_parser("status", help="Show Gunicorn service status") + p_status.add_argument("domain", help="Domain name") + + return parser + + +def main() -> int: + # Convenience shorthand: if first arg is a domain or not a command, default to 'setup' + known_commands = { + "setup", "repair", "doctor", "install-templates", + "update", "install", "status", "-h", "--help", + } + if len(sys.argv) > 1 and sys.argv[1] not in known_commands: + sys.argv.insert(1, "setup") + + parser = build_parser() + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 0 + + dispatch = { + "setup": cmd_setup, + "repair": cmd_repair, + "doctor": cmd_doctor, + "install-templates": cmd_install_templates, + "update": cmd_update, + "install": cmd_install, + "status": cmd_status, + } + + handler = dispatch.get(args.command) + if handler is None: + parser.print_help() + return 1 + + try: + return handler(args) + except Exception as exc: + error(f"Unexpected error: {exc}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/hdg b/hdg new file mode 100644 index 0000000..5332cd9 --- /dev/null +++ b/hdg @@ -0,0 +1,342 @@ +#!/usr/bin/env bash +# ────────────────────────────────────────────────────────────────────────── +# hdg — HestiaCP + Django + Gunicorn Global CLI +# ────────────────────────────────────────────────────────────────────────── +# A fast, global tool for deploying and managing Django on HestiaCP. +# +# Usage from ANYWHERE on your system: +# hdg [project] # Quick deploy (auto-detects project) +# hdg setup [project] # Explicit deploy +# hdg repair # Fix & migrate existing setup +# hdg doctor # Run health checks +# hdg status # Check service status +# hdg update # Self-update to latest version +# hdg install # Re-link global commands +# +# Prepared by: MohammadAmin Baranzehi — amin.baranzehi.com +# ────────────────────────────────────────────────────────────────────────── + +set -euo pipefail + +# ── Resolve real project directory even through symlinks ────────────────── +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ]; do + DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)" + SOURCE="$(readlink "$SOURCE")" + [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" +done +PROJECT_ROOT="$(cd -P "$(dirname "$SOURCE")" && pwd)" +CLI_PY="${PROJECT_ROOT}/cli.py" + +# ── Colours ─────────────────────────────────────────────────────────────── +RED='\033[0;91m' +GREEN='\033[0;92m' +YELLOW='\033[0;93m' +CYAN='\033[0;96m' +BOLD='\033[1m' +NC='\033[0m' + +info() { echo -e " ${CYAN}→${NC} $*"; } +success() { echo -e " ${GREEN}✔${NC} $*"; } +warn() { echo -e " ${YELLOW}⚠${NC} $*"; } +error() { echo -e " ${RED}✘${NC} $*"; } + +# ── Self-installation to /usr/local/bin ─────────────────────────────────── +install_global_links() { + local bin_dir="/usr/local/bin" + [ ! -d "$bin_dir" ] && return 0 + + local links=("hdg" "v-django" "hestia-django") + local need_install=0 + + for link in "${links[@]}"; do + local target="${bin_dir}/${link}" + if [ ! -L "$target" ] || [ "$(readlink -f "$target" 2>/dev/null)" != "${PROJECT_ROOT}/hdg" ]; then + need_install=1 + break + fi + done + + if [ "$need_install" -eq 1 ]; then + local SUDO="" + [ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &>/dev/null && SUDO="sudo" + + for link in "${links[@]}"; do + $SUDO ln -sf "${PROJECT_ROOT}/hdg" "${bin_dir}/${link}" 2>/dev/null || true + done + $SUDO chmod +x "${PROJECT_ROOT}/hdg" 2>/dev/null || true + + if [ ! -f "${PROJECT_ROOT}/.installed" ]; then + touch "${PROJECT_ROOT}/.installed" 2>/dev/null || true + echo "" + success "Installed globally as '${BOLD}hdg${NC}' (also '${BOLD}v-django${NC}')" + info "You can now run '${BOLD}hdg ${NC}' from ANY directory!" + echo "" + fi + fi +} + +# ── Self-update command ─────────────────────────────────────────────────── +self_update() { + echo "" + echo -e "${BOLD}Updating hdg (HestiaCP Django-Gunicorn Engine)...${NC}" + + if [ -d "${PROJECT_ROOT}/.git" ] && command -v git &>/dev/null; then + info "Pulling latest changes from git repository..." + git -C "$PROJECT_ROOT" pull --ff-only 2>/dev/null || { + warn "git pull --ff-only failed, trying standard pull..." + git -C "$PROJECT_ROOT" pull || warn "Could not pull git updates (check network or local changes)" + } + else + info "Git repository not detected, skipping git pull." + fi + + local hestia_tpl_dir="/usr/local/hestia/data/templates/web/nginx" + if [ -d "$hestia_tpl_dir" ]; then + info "Updating NGINX templates in HestiaCP..." + local SUDO="" + [ "${EUID:-$(id -u)}" -ne 0 ] && command -v sudo &>/dev/null && SUDO="sudo" + $SUDO cp -f "${PROJECT_ROOT}/templates/django-gunicorn.tpl" "$hestia_tpl_dir/" 2>/dev/null || true + $SUDO cp -f "${PROJECT_ROOT}/templates/django-gunicorn.stpl" "$hestia_tpl_dir/" 2>/dev/null || true + success "NGINX templates updated in ${hestia_tpl_dir}" + fi + + install_global_links + success "hdg is up to date and ready!" + echo "" + exit 0 +} + +# ── Handle self-install / update directly ───────────────────────────────── +case "${1:-}" in + install|self-install) + install_global_links + success "Global installation complete! Run '${BOLD}hdg${NC}' from anywhere." + exit 0 + ;; + update|self-update) + self_update + ;; +esac + +# ── Automatically install or update global symlink on run ──────────────── +install_global_links 2>/dev/null || true + +# ── Try Python engine first ────────────────────────────────────────────── +if command -v python3 &>/dev/null && [ -f "$CLI_PY" ]; then + python3 "$CLI_PY" "$@" + exit $? +fi + +# ══════════════════════════════════════════════════════════════════════════ +# Bash fallback (self-contained, for servers without full Python setup) +# ══════════════════════════════════════════════════════════════════════════ + +usage() { + echo -e "${BOLD}hdg${NC} — HestiaCP + Django + Gunicorn CLI" + echo "" + echo "Usage:" + echo " hdg [project] # Auto-deploy" + echo " hdg repair # Fix existing deployment" + echo " hdg doctor # Check deployment health" + echo " hdg update # Self-update" + echo "" + echo "Examples:" + echo " hdg example.com admin myproject" + echo " hdg example.com admin # auto-detects config/wsgi.py" + echo " hdg repair example.com admin" + echo " hdg update" + exit 1 +} + +# ── Handle 'repair' ─────────────────────────────────────────────────────── +if [ "${1:-}" = "repair" ]; then + [ "$#" -lt 3 ] && usage + DOMAIN="$2" + USER="$3" + SERVICE_NAME="gunicorn-${DOMAIN}" + SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + + if [ ! -f "$SERVICE_FILE" ]; then + error "Service file not found: $SERVICE_FILE" + exit 1 + fi + + info "Repairing deployment for: $DOMAIN" + BACKUP="${SERVICE_FILE}.bak.$(date +%Y%m%d_%H%M%S)" + cp "$SERVICE_FILE" "$BACKUP" + success "Backed up to: $BACKUP" + + "$0" "$DOMAIN" "$USER" + exit $? +fi + +# ── Handle 'setup' explicit keyword ────────────────────────────────────── +if [ "${1:-}" = "setup" ]; then + shift +fi + +# ── Parse arguments ────────────────────────────────────────────────────── +[ "$#" -lt 2 ] && usage + +DOMAIN="$1" +USER="$2" +PROJECT="${3:-}" + +HOMEDIR="/home/$USER" +WEBDIR="$HOMEDIR/web/$DOMAIN/public_html" +SERVICE_NAME="gunicorn-${DOMAIN}" +SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" + +if [ ! -d "$WEBDIR" ]; then + error "Domain directory not found → $WEBDIR" + exit 1 +fi + +# ── Virtualenv detection ───────────────────────────────────────────────── +if [ -d "$WEBDIR/venv" ]; then + VENV_BIN="$WEBDIR/venv/bin" +elif [ -d "$WEBDIR/.venv" ]; then + VENV_BIN="$WEBDIR/.venv/bin" +elif [ -d "$HOMEDIR/.virtualenvs/$DOMAIN" ]; then + VENV_BIN="$HOMEDIR/.virtualenvs/$DOMAIN/bin" +else + error "Virtual environment not found in:" + error " $WEBDIR/venv" + error " $WEBDIR/.venv" + error " $HOMEDIR/.virtualenvs/$DOMAIN" + exit 1 +fi + +if [ ! -f "$VENV_BIN/gunicorn" ]; then + error "Gunicorn is not installed in the virtualenv ($VENV_BIN/gunicorn missing)." + error "Install it: $VENV_BIN/pip install gunicorn" + exit 1 +fi + +# ── WSGI auto-detection ────────────────────────────────────────────────── +WSGI_MODULE="" + +if [ -n "$PROJECT" ] && [ -f "$WEBDIR/$PROJECT/wsgi.py" ]; then + WSGI_MODULE="${PROJECT}.wsgi:application" + info "WSGI module (specified): $WSGI_MODULE" +elif [ -f "$WEBDIR/config/wsgi.py" ]; then + WSGI_MODULE="config.wsgi:application" + info "WSGI module (detected modular): $WSGI_MODULE" +else + DETECTED_WSGI=$(find "$WEBDIR" -maxdepth 3 -name "wsgi.py" \ + -not -path "*/venv/*" \ + -not -path "*/.venv/*" \ + -not -path "*/site-packages/*" \ + | head -n 1) + + if [ -n "$DETECTED_WSGI" ]; then + REL_WSGI="${DETECTED_WSGI#$WEBDIR/}" + MODULE_PATH="${REL_WSGI%/wsgi.py}" + MODULE_DOT=$(echo "$MODULE_PATH" | tr '/' '.') + + if [ "$REL_WSGI" = "wsgi.py" ]; then + WSGI_MODULE="wsgi:application" + else + WSGI_MODULE="${MODULE_DOT}.wsgi:application" + fi + info "WSGI module (auto-detected): $WSGI_MODULE" + else + error "Could not auto-detect wsgi.py in $WEBDIR" + error "Please specify the project name: $0 $DOMAIN $USER " + exit 1 + fi +fi + +# ── Environment & Workers ──────────────────────────────────────────────── +ENV_FILE_LINE="" +if [ -f "$WEBDIR/.env" ]; then + ENV_FILE_LINE="EnvironmentFile=$WEBDIR/.env" + info "Environment file detected: $WEBDIR/.env" +fi + +CPU_COUNT=$(nproc 2>/dev/null || echo 1) +WORKERS=$(( (2 * CPU_COUNT) + 1 )) +[ "$WORKERS" -gt 9 ] && WORKERS=9 +SOCK_PATH="$WEBDIR/gunicorn.sock" + +# ── Print summary ──────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Deployment Summary${NC}" +echo " Domain : $DOMAIN" +echo " User : $USER" +echo " WSGI : $WSGI_MODULE" +echo " Workers : $WORKERS" +echo " Socket : $SOCK_PATH" +echo " Service : $SERVICE_NAME" +echo "" + +# ── Generate service file ──────────────────────────────────────────────── +if [ -f "$SERVICE_FILE" ]; then + BACKUP="${SERVICE_FILE}.bak.$(date +%Y%m%d_%H%M%S)" + cp "$SERVICE_FILE" "$BACKUP" + warn "Existing service backed up to: $BACKUP" +fi + +cat < /dev/null +# ────────────────────────────────────────────────────────────────── +# Gunicorn service for $DOMAIN +# Auto-generated by hdg (HestiaCP Django-Gunicorn Engine) +# ────────────────────────────────────────────────────────────────── +[Unit] +Description=Gunicorn instance for $DOMAIN (Django) +After=network.target + +[Service] +User=$USER +Group=www-data +WorkingDirectory=$WEBDIR +Environment="PATH=$VENV_BIN:/usr/local/bin:/usr/bin" +Environment="PYTHONUNBUFFERED=1" +$ENV_FILE_LINE +ExecStart=$VENV_BIN/gunicorn \\ + --access-logfile - \\ + --error-logfile - \\ + --workers $WORKERS \\ + --threads 2 \\ + --worker-class=gthread \\ + --bind unix:$SOCK_PATH \\ + --umask 007 \\ + --log-level=info \\ + $WSGI_MODULE + +Restart=always +RestartSec=3 +ProtectSystem=full +PrivateTmp=true +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target +EOF + +success "Generated: $SERVICE_FILE" + +# ── Fix database permissions ───────────────────────────────────────────── +if [ -f "$WEBDIR/db.sqlite3" ]; then + sudo chown "$USER:www-data" "$WEBDIR/db.sqlite3" + sudo chmod 664 "$WEBDIR/db.sqlite3" + success "Fixed SQLite permissions on db.sqlite3" +fi + +# ── Enable & start service ─────────────────────────────────────────────── +sudo systemctl daemon-reload +sudo systemctl enable "$SERVICE_NAME" +sudo systemctl restart "$SERVICE_NAME" + +sleep 1 +if sudo systemctl is-active --quiet "$SERVICE_NAME"; then + success "Service $SERVICE_NAME is running!" +else + error "Service failed to start. Logs:" + sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager + exit 1 +fi + +echo "" +echo -e "${GREEN}${BOLD}Deployment completed successfully!${NC}"