Files
unitforge/backend/app/core/templates.py
William Valentin 860f60591c Fix contrast issues with text-muted and bg-dark classes
- Fixed Bootstrap bg-dark class to use better contrasting color
- Added comprehensive text-muted contrast fixes for various contexts
- Improved dark theme colors for better accessibility
- Fixed CSS inheritance issues for code elements in dark contexts
- All color choices meet WCAG AA contrast requirements
2025-09-14 14:58:35 -07:00

659 lines
23 KiB
Python

"""
Template system for generating common systemd unit file configurations.
This module provides pre-built templates for common service types and use cases,
making it easy to generate properly configured unit files for typical scenarios.
"""
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from .unit_file import SystemdUnitFile, UnitType, create_unit_file
@dataclass
class TemplateParameter:
"""Defines a parameter that can be configured in a template."""
name: str
description: str
parameter_type: str # string, integer, boolean, choice, list
required: bool = True
default: Optional[Any] = None
choices: Optional[List[str]] = None
example: Optional[str] = None
@dataclass
class UnitTemplate:
"""Represents a systemd unit file template."""
name: str
description: str
unit_type: UnitType
category: str
parameters: List[TemplateParameter]
tags: List[str] = field(default_factory=list)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate a unit file from this template with the given parameters."""
raise NotImplementedError("Subclasses must implement generate method")
class WebApplicationTemplate(UnitTemplate):
"""Template for web application services."""
def __init__(self):
super().__init__(
name="webapp",
description="Web application service (Node.js, Python, etc.)",
unit_type=UnitType.SERVICE,
category="Web Services",
parameters=[
TemplateParameter("name", "Service name", "string", example="myapp"),
TemplateParameter(
"description",
"Service description",
"string",
example="My Web Application",
),
TemplateParameter(
"exec_start",
"Command to start the application",
"string",
example="/usr/bin/node /opt/myapp/server.js",
),
TemplateParameter(
"user",
"User to run the service as",
"string",
default="www-data",
example="myapp",
),
TemplateParameter(
"group",
"Group to run the service as",
"string",
default="www-data",
example="myapp",
),
TemplateParameter(
"working_directory",
"Working directory",
"string",
example="/opt/myapp",
),
TemplateParameter(
"environment_file",
"Environment file path",
"string",
required=False,
example="/etc/myapp/environment",
),
TemplateParameter(
"port", "Port number", "integer", required=False, example="3000"
),
TemplateParameter(
"restart_policy",
"Restart policy",
"choice",
default="on-failure",
choices=["no", "always", "on-failure", "on-success"],
),
TemplateParameter(
"private_tmp", "Use private /tmp", "boolean", default=True
),
TemplateParameter(
"protect_system",
"Protect system directories",
"choice",
default="strict",
choices=["no", "yes", "strict", "full"],
),
],
tags=["web", "application", "nodejs", "python", "service"],
)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate web application service unit file."""
kwargs = {
"description": params.get(
"description", f"{params['name']} web application"
),
"service_type": "simple",
"exec_start": params["exec_start"],
"user": params.get("user", "www-data"),
"group": params.get("group", "www-data"),
"restart": params.get("restart_policy", "on-failure"),
"wanted_by": ["multi-user.target"],
}
if params.get("working_directory"):
kwargs["working_directory"] = params["working_directory"]
unit = create_unit_file(UnitType.SERVICE, **kwargs)
# Add additional security and configuration
if params.get("environment_file"):
unit.set_value("Service", "EnvironmentFile", params["environment_file"])
if params.get("port"):
unit.set_value("Service", "Environment", f"PORT={params['port']}")
# Security hardening
if params.get("private_tmp", True):
unit.set_value("Service", "PrivateTmp", "yes")
protect_system = params.get("protect_system", "strict")
if protect_system != "no":
unit.set_value("Service", "ProtectSystem", protect_system)
unit.set_value("Service", "NoNewPrivileges", "yes")
unit.set_value("Service", "ProtectKernelTunables", "yes")
unit.set_value("Service", "ProtectControlGroups", "yes")
unit.set_value("Service", "RestrictSUIDSGID", "yes")
return unit
class DatabaseTemplate(UnitTemplate):
"""Template for database services."""
def __init__(self):
super().__init__(
name="database",
description="Database service (PostgreSQL, MySQL, MongoDB, etc.)",
unit_type=UnitType.SERVICE,
category="Database Services",
parameters=[
TemplateParameter(
"name", "Database service name", "string", example="postgresql"
),
TemplateParameter(
"description",
"Service description",
"string",
example="PostgreSQL Database Server",
),
TemplateParameter(
"exec_start",
"Database start command",
"string",
example=(
"/usr/lib/postgresql/13/bin/postgres "
"-D /var/lib/postgresql/13/main"
),
),
TemplateParameter(
"user", "Database user", "string", example="postgres"
),
TemplateParameter(
"group", "Database group", "string", example="postgres"
),
TemplateParameter(
"data_directory",
"Data directory",
"string",
example="/var/lib/postgresql/13/main",
),
TemplateParameter(
"pid_file",
"PID file path",
"string",
required=False,
example="/var/run/postgresql/13-main.pid",
),
TemplateParameter(
"timeout_sec", "Startup timeout", "integer", default=300
),
],
tags=["database", "postgresql", "mysql", "mongodb", "service"],
)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate database service unit file."""
kwargs = {
"description": params.get(
"description", f"{params['name']} Database Server"
),
"service_type": "notify",
"exec_start": params["exec_start"],
"user": params["user"],
"group": params["group"],
"restart": "on-failure",
"wanted_by": ["multi-user.target"],
"after": ["network.target"],
}
unit = create_unit_file(UnitType.SERVICE, **kwargs)
# Database-specific configuration
if params.get("data_directory"):
unit.set_value("Service", "WorkingDirectory", params["data_directory"])
if params.get("pid_file"):
unit.set_value("Service", "PIDFile", params["pid_file"])
timeout = params.get("timeout_sec", 300)
unit.set_value("Service", "TimeoutSec", str(timeout))
# Security settings for databases
unit.set_value("Service", "PrivateTmp", "yes")
unit.set_value("Service", "ProtectSystem", "strict")
unit.set_value("Service", "NoNewPrivileges", "yes")
unit.set_value("Service", "PrivateDevices", "yes")
return unit
class BackupTimerTemplate(UnitTemplate):
"""Template for backup timer services."""
def __init__(self):
super().__init__(
name="backup-timer",
description="Scheduled backup service with timer",
unit_type=UnitType.TIMER,
category="System Maintenance",
parameters=[
TemplateParameter(
"name", "Backup job name", "string", example="daily-backup"
),
TemplateParameter(
"description",
"Backup description",
"string",
example="Daily database backup",
),
TemplateParameter(
"schedule",
"Backup schedule",
"choice",
choices=["daily", "weekly", "monthly", "custom"],
default="daily",
),
TemplateParameter(
"custom_schedule",
"Custom schedule (OnCalendar format)",
"string",
required=False,
example="*-*-* 02:00:00",
),
TemplateParameter(
"backup_script",
"Backup script path",
"string",
example="/usr/local/bin/backup.sh",
),
TemplateParameter(
"backup_user", "User to run backup as", "string", default="backup"
),
TemplateParameter(
"persistent", "Run missed backups on boot", "boolean", default=True
),
TemplateParameter(
"randomized_delay",
"Randomized delay in minutes",
"integer",
required=False,
default=0,
),
],
tags=["backup", "timer", "maintenance", "scheduled"],
)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate backup timer and service unit files."""
# Create the timer unit
schedule_map = {"daily": "daily", "weekly": "weekly", "monthly": "monthly"}
schedule = params.get("schedule", "daily")
if schedule == "custom":
calendar_spec = params.get("custom_schedule", "daily")
else:
calendar_spec = schedule_map.get(schedule, "daily")
timer_kwargs = {
"description": f"{params.get('description', params['name'])} Timer",
"on_calendar": calendar_spec,
"persistent": str(params.get("persistent", True)).lower(),
"wanted_by": ["timers.target"],
}
timer_unit = create_unit_file(UnitType.TIMER, **timer_kwargs)
# Add randomized delay if specified
if params.get("randomized_delay", 0) > 0:
delay_sec = params["randomized_delay"] * 60 # Convert minutes to seconds
timer_unit.set_value("Timer", "RandomizedDelaySec", f"{delay_sec}s")
# For this template, we return the timer unit (service would be separate)
return timer_unit
class ProxySocketTemplate(UnitTemplate):
"""Template for socket-activated proxy services."""
def __init__(self):
super().__init__(
name="proxy-socket",
description="Socket-activated proxy service",
unit_type=UnitType.SOCKET,
category="Network Services",
parameters=[
TemplateParameter(
"name", "Socket service name", "string", example="myapp-proxy"
),
TemplateParameter(
"description",
"Socket description",
"string",
example="Proxy socket for myapp",
),
TemplateParameter(
"listen_port", "Port to listen on", "integer", example="8080"
),
TemplateParameter(
"listen_address",
"Address to bind to",
"string",
default="0.0.0.0", # nosec B104
example="127.0.0.1",
),
TemplateParameter(
"socket_user",
"Socket owner user",
"string",
required=False,
example="www-data",
),
TemplateParameter(
"socket_group",
"Socket owner group",
"string",
required=False,
example="www-data",
),
TemplateParameter(
"socket_mode",
"Socket file permissions",
"string",
default="0644",
example="0660",
),
TemplateParameter(
"accept", "Accept multiple connections", "boolean", default=False
),
TemplateParameter(
"max_connections",
"Maximum connections",
"integer",
required=False,
default=64,
),
],
tags=["socket", "proxy", "network", "activation"],
)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate socket unit file."""
listen_address = params.get("listen_address", "0.0.0.0") # nosec B104
listen_port = params["listen_port"]
listen_spec = f"{listen_address}:{listen_port}"
kwargs = {
"description": params.get("description", f"{params['name']} socket"),
"listen_stream": listen_spec,
"wanted_by": ["sockets.target"],
}
unit = create_unit_file(UnitType.SOCKET, **kwargs)
# Socket-specific configuration
if params.get("socket_user"):
unit.set_value("Socket", "SocketUser", params["socket_user"])
if params.get("socket_group"):
unit.set_value("Socket", "SocketGroup", params["socket_group"])
socket_mode = params.get("socket_mode", "0644")
unit.set_value("Socket", "SocketMode", socket_mode)
if params.get("accept", False):
unit.set_value("Socket", "Accept", "yes")
max_conn = params.get("max_connections")
if max_conn:
unit.set_value("Socket", "MaxConnections", str(max_conn))
# Security settings
unit.set_value("Socket", "FreeBind", "true")
unit.set_value("Socket", "NoDelay", "true")
return unit
class ContainerTemplate(UnitTemplate):
"""Template for containerized services (Docker/Podman)."""
def __init__(self):
super().__init__(
name="container",
description="Containerized service (Docker/Podman)",
unit_type=UnitType.SERVICE,
category="Container Services",
parameters=[
TemplateParameter(
"name",
"Container service name",
"string",
example="webapp-container",
),
TemplateParameter(
"description",
"Container description",
"string",
example="My Web App Container",
),
TemplateParameter(
"container_runtime",
"Container runtime",
"choice",
choices=["docker", "podman"],
default="docker",
),
TemplateParameter(
"image", "Container image", "string", example="nginx:latest"
),
TemplateParameter(
"ports",
"Port mappings",
"string",
required=False,
example="80:8080,443:8443",
),
TemplateParameter(
"volumes",
"Volume mounts",
"string",
required=False,
example="/data:/app/data,/config:/app/config",
),
TemplateParameter(
"environment",
"Environment variables",
"string",
required=False,
example="ENV=production,DEBUG=false",
),
TemplateParameter(
"network",
"Container network",
"string",
required=False,
example="bridge",
),
TemplateParameter(
"restart_policy",
"Container restart policy",
"choice",
choices=["no", "always", "unless-stopped", "on-failure"],
default="unless-stopped",
),
TemplateParameter(
"pull_policy",
"Image pull policy",
"choice",
choices=["always", "missing", "never"],
default="missing",
),
],
tags=["container", "docker", "podman", "service"],
)
def generate(self, params: Dict[str, Any]) -> SystemdUnitFile:
"""Generate container service unit file."""
runtime = params.get("container_runtime", "docker")
image = params["image"]
# Build container run command
run_cmd = [runtime, "run", "--rm"]
# Add port mappings
if params.get("ports"):
for port_map in params["ports"].split(","):
port_map = port_map.strip()
if port_map:
run_cmd.extend(["-p", port_map])
# Add volume mounts
if params.get("volumes"):
for volume in params["volumes"].split(","):
volume = volume.strip()
if volume:
run_cmd.extend(["-v", volume])
# Add environment variables
if params.get("environment"):
for env_var in params["environment"].split(","):
env_var = env_var.strip()
if env_var:
run_cmd.extend(["-e", env_var])
# Add network
if params.get("network"):
run_cmd.extend(["--network", params["network"]])
# Add container name
container_name = f"{params['name']}-container"
run_cmd.extend(["--name", container_name])
# Add image
run_cmd.append(image)
exec_start = " ".join(run_cmd)
# Pre-start commands
pre_start_cmds = []
# Pull image if policy requires it
pull_policy = params.get("pull_policy", "missing")
if pull_policy == "always":
pre_start_cmds.append(f"{runtime} pull {image}")
# Stop and remove existing container
pre_start_cmds.append(f"-{runtime} stop {container_name}")
pre_start_cmds.append(f"-{runtime} rm {container_name}")
kwargs = {
"description": params.get(
"description", f"{params['name']} container service"
),
"service_type": "simple",
"exec_start": exec_start,
"restart": "always",
"wanted_by": ["multi-user.target"],
"after": ["docker.service"] if runtime == "docker" else ["podman.service"],
}
unit = create_unit_file(UnitType.SERVICE, **kwargs)
# Add pre-start commands
for cmd in pre_start_cmds:
unit.set_value("Service", "ExecStartPre", cmd)
# Add stop command
unit.set_value("Service", "ExecStop", f"{runtime} stop {container_name}")
unit.set_value("Service", "ExecStopPost", f"{runtime} rm {container_name}")
# Container-specific settings
unit.set_value("Service", "TimeoutStartSec", "300")
unit.set_value("Service", "TimeoutStopSec", "30")
return unit
class TemplateRegistry:
"""Registry for managing available unit file templates."""
def __init__(self):
self._templates = {}
self._register_default_templates()
def _register_default_templates(self):
"""Register all default templates."""
templates = [
WebApplicationTemplate(),
DatabaseTemplate(),
BackupTimerTemplate(),
ProxySocketTemplate(),
ContainerTemplate(),
]
for template in templates:
self.register(template)
def register(self, template: UnitTemplate):
"""Register a new template."""
self._templates[template.name] = template
def get_template(self, name: str) -> Optional[UnitTemplate]:
"""Get a template by name."""
return self._templates.get(name)
def list_templates(self) -> List[UnitTemplate]:
"""Get all available templates."""
return list(self._templates.values())
def get_by_category(self, category: str) -> List[UnitTemplate]:
"""Get templates by category."""
return [t for t in self._templates.values() if t.category == category]
def get_by_type(self, unit_type: UnitType) -> List[UnitTemplate]:
"""Get templates by unit type."""
return [t for t in self._templates.values() if t.unit_type == unit_type]
def search(self, query: str) -> List[UnitTemplate]:
"""Search templates by name, description, or tags."""
query = query.lower()
results = []
for template in self._templates.values():
# Search in name and description
if query in template.name.lower() or query in template.description.lower():
results.append(template)
continue
# Search in tags
if template.tags:
for tag in template.tags:
if query in tag.lower():
results.append(template)
break
return results
# Global template registry instance
template_registry = TemplateRegistry()