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
This commit is contained in:
84
core/service_generator.py
Normal file
84
core/service_generator.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- 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,
|
||||
)
|
||||
Reference in New Issue
Block a user