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
This commit is contained in:
658
backend/app/core/templates.py
Normal file
658
backend/app/core/templates.py
Normal file
@@ -0,0 +1,658 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user