Files
hdg/core/service_generator.py
MohammadAmin Baranzehi 8a9269bb79 feat(core): implement modular OOP engine following SOLID and DRY principles
- 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
2026-09-14 13:54:05 +03:30

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