- models: DomainConfig, ServiceConfig, DiagnosticReport (single source of truth) - interfaces: DIP & ISP contracts for detector, generator, executor, doctor, and migrator - detector: Smart auto-detection for modular (config/wsgi.py), traditional, and flat layouts - service_generator: Systemd service generation with --umask 007 and security sandboxing - executor: Linux command execution with dry-run support - nginx_manager: HestiaCP template installation and validation - migrator: Automated migration and repair of existing deployments - doctor: Comprehensive post-deployment health check engine
85 lines
3.0 KiB
Python
85 lines
3.0 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Systemd service unit file generator for Gunicorn.
|
|
|
|
**Single Responsibility:** Accept a ``ServiceConfig`` and render a production-
|
|
hardened systemd ``.service`` file. Security best-practices (``--umask 007``,
|
|
``ProtectSystem``, dynamic worker count) are baked in by default.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import textwrap
|
|
|
|
from core.interfaces import IServiceGenerator
|
|
from core.models import ServiceConfig
|
|
|
|
|
|
class SystemdServiceGenerator(IServiceGenerator):
|
|
"""Generates a systemd service unit for a Gunicorn-backed Django app."""
|
|
|
|
_TEMPLATE = textwrap.dedent("""\
|
|
# ──────────────────────────────────────────────────────────────────
|
|
# Gunicorn service for {domain}
|
|
# Auto-generated by hestia-django-gunicorn deployment engine
|
|
# ──────────────────────────────────────────────────────────────────
|
|
[Unit]
|
|
Description=Gunicorn instance for {domain} (Django)
|
|
After=network.target
|
|
|
|
[Service]
|
|
User={user}
|
|
Group=www-data
|
|
WorkingDirectory={web_dir}
|
|
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 {threads} \\
|
|
--worker-class=gthread \\
|
|
--bind unix:{socket_path} \\
|
|
--umask {umask} \\
|
|
--log-level={log_level} \\
|
|
{wsgi_module}
|
|
|
|
Restart=always
|
|
RestartSec={restart_sec}
|
|
ProtectSystem=full
|
|
PrivateTmp=true
|
|
NoNewPrivileges=true
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
""")
|
|
|
|
def generate(self, config: ServiceConfig) -> str:
|
|
"""Return the full text of a systemd ``.service`` unit file."""
|
|
dc = config.domain_config
|
|
pi = config.project_info
|
|
|
|
venv_bin = str(pi.venv_path / "bin") if pi.venv_path else "/usr/bin"
|
|
|
|
# Optional EnvironmentFile directive for projects with .env
|
|
if pi.has_dot_env:
|
|
env_file_line = f"EnvironmentFile={dc.web_dir}/.env"
|
|
else:
|
|
env_file_line = "# No .env found — environment variables from Environment= only"
|
|
|
|
return self._TEMPLATE.format(
|
|
domain=dc.domain,
|
|
user=dc.user,
|
|
web_dir=dc.web_dir,
|
|
venv_bin=venv_bin,
|
|
socket_path=dc.socket_path,
|
|
workers=config.workers,
|
|
threads=config.threads,
|
|
umask=config.umask,
|
|
log_level=config.log_level,
|
|
restart_sec=config.restart_sec,
|
|
wsgi_module=pi.wsgi_module,
|
|
env_file_line=env_file_line,
|
|
)
|