- 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
659 lines
23 KiB
Python
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()
|