# -*- 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, )