Files
haproxy-dashboard/utils/config_generator.py
Mateusz Gruszczyński addb21bc3e rewrite
2025-11-04 09:56:37 +01:00

223 lines
7.7 KiB
Python

"""HAProxy Config Generator - Build config from database"""
import os
import subprocess
import shutil
from datetime import datetime
from database import db
from database.models import VirtualHost, Certificate, HAPROXY_STATS_PORT
from config.settings import HAPROXY_CONFIG_PATH, HAPROXY_BACKUP_DIR
import logging
logger = logging.getLogger(__name__)
def generate_haproxy_config():
"""Generate HAProxy config from database"""
config_lines = []
# ===== GLOBAL SECTION =====
config_lines.extend([
"# HAProxy Configuration - Auto-generated by HAProxy Manager",
f"# Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
"global",
" log stdout local0",
" log stdout local1 notice",
" chroot /var/lib/haproxy",
" stats socket /run/haproxy/admin.sock mode 660 level admin",
" stats timeout 30s",
" user haproxy",
" group haproxy",
" daemon",
" maxconn 4096",
" tune.ssl.default-dh-param 2048",
"",
])
# ===== DEFAULTS SECTION =====
config_lines.extend([
"defaults",
" log global",
" mode http",
" option httplog",
" option dontlognull",
" option http-server-close",
" option forwardfor except 127.0.0.0/8",
" option redispatch",
" retries 3",
" timeout connect 5000",
" timeout client 50000",
" timeout server 50000",
" errorfile 400 /etc/haproxy/errors/400.http",
" errorfile 403 /etc/haproxy/errors/403.http",
" errorfile 408 /etc/haproxy/errors/408.http",
" errorfile 500 /etc/haproxy/errors/500.http",
" errorfile 502 /etc/haproxy/errors/502.http",
" errorfile 503 /etc/haproxy/errors/503.http",
" errorfile 504 /etc/haproxy/errors/504.http",
"",
])
# ===== STATS FRONTEND (HARDCODED :8404) =====
config_lines.extend([
"# HAProxy Statistics - Hardcoded on port 8404",
"frontend stats",
f" bind *:{HAPROXY_STATS_PORT}",
" stats enable",
" stats admin if TRUE",
" stats uri /stats",
" stats refresh 30s",
"",
])
# ===== FRONTENDS & BACKENDS FROM DATABASE =====
vhosts = VirtualHost.query.filter_by(enabled=True).all()
for vhost in vhosts:
# Skip if no backend servers
if not vhost.backend_servers:
logger.warning(f"[CONFIG] VHost '{vhost.name}' has no backend servers, skipping")
continue
# ===== FRONTEND =====
config_lines.append(f"# VHost: {vhost.name}")
config_lines.append(f"frontend {vhost.name}")
config_lines.append(f" description {vhost.hostname}")
config_lines.append(f" bind {vhost.frontend_ip}:{vhost.frontend_port}")
# SSL config
if vhost.use_ssl and vhost.certificate_id:
cert = Certificate.query.get(vhost.certificate_id)
if cert:
# Certificate file path
cert_path = f"/app/uploads/certificates/{cert.name}.pem"
config_lines.append(f" ssl crt {cert_path}")
config_lines.append(f" mode {vhost.protocol}")
config_lines.append(f" option httplog")
# Custom headers
if vhost.del_server_header:
config_lines.append(" http-response del-header Server")
if vhost.add_custom_header and vhost.custom_header_name:
config_lines.append(f" http-response add-header {vhost.custom_header_name} {vhost.custom_header_value}")
if vhost.forward_for:
config_lines.append(" option forwardfor except 127.0.0.0/8")
# Security rules
if vhost.dos_protection:
config_lines.extend([
f" stick-table type ip size 100k expire {vhost.dos_ban_duration} store http_req_rate(10s)",
f" http-request track-sc0 src",
f" http-request deny if {{ sc_http_req_rate(0) gt {vhost.dos_limit_requests} }}",
])
if vhost.sql_injection_check:
config_lines.append(" # SQL Injection protection enabled")
if vhost.xss_check:
config_lines.append(" # XSS protection enabled")
if vhost.webshell_check:
config_lines.append(" # WebShell protection enabled")
config_lines.append(f" default_backend {vhost.name}_backend")
config_lines.append("")
# ===== BACKEND =====
config_lines.append(f"backend {vhost.name}_backend")
config_lines.append(f" balance {vhost.lb_method}")
config_lines.append(f" option httpchk GET / HTTP/1.1\\r\\nHost:\\ www")
# Backend servers
for i, server in enumerate(vhost.backend_servers, 1):
if not server.enabled:
continue
server_line = f" server {server.name} {server.ip_address}:{server.port}"
if server.weight != 1:
server_line += f" weight {server.weight}"
if server.maxconn:
server_line += f" maxconn {server.maxconn}"
if server.health_check:
server_line += f" check inter 5000 rise 2 fall 3"
config_lines.append(server_line)
config_lines.append("")
return "\n".join(config_lines)
def save_haproxy_config(config_content):
"""Save config to file"""
try:
# Create backup
if os.path.exists(HAPROXY_CONFIG_PATH):
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = os.path.join(HAPROXY_BACKUP_DIR, f'haproxy_backup_{timestamp}.cfg')
shutil.copy2(HAPROXY_CONFIG_PATH, backup_path)
logger.info(f"[CONFIG] Backup created: {backup_path}", flush=True)
# Write new config
with open(HAPROXY_CONFIG_PATH, 'w') as f:
f.write(config_content)
logger.info(f"[CONFIG] Config saved to {HAPROXY_CONFIG_PATH}", flush=True)
return True
except Exception as e:
logger.error(f"[CONFIG] Error saving config: {e}", flush=True)
return False
def reload_haproxy():
"""Reload HAProxy service"""
try:
# Generate config
config_content = generate_haproxy_config()
# Test config syntax
result = subprocess.run(
['haproxy', '-c', '-f', HAPROXY_CONFIG_PATH],
capture_output=True,
text=True,
timeout=5
)
if result.returncode != 0:
logger.error(f"[CONFIG] HAProxy syntax error: {result.stderr}", flush=True)
return False
# Save config
if not save_haproxy_config(config_content):
return False
# Reload HAProxy
result = subprocess.run(
['systemctl', 'reload', 'haproxy'],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
logger.info("[CONFIG] HAProxy reloaded successfully", flush=True)
return True
else:
logger.error(f"[CONFIG] HAProxy reload failed: {result.stderr}", flush=True)
return False
except subprocess.TimeoutExpired:
logger.error("[CONFIG] HAProxy reload timeout", flush=True)
return False
except Exception as e:
logger.error(f"[CONFIG] Error reloading HAProxy: {e}", flush=True)
return False