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:
William Valentin
2025-09-14 14:58:35 -07:00
commit 860f60591c
37 changed files with 11599 additions and 0 deletions

29
backend/app/__init__.py Normal file
View File

@@ -0,0 +1,29 @@
"""
UnitForge Application Package
This package contains the main application components:
- Core unit file functionality (parsing, validation, generation)
- Template system for common systemd unit configurations
- FastAPI web application and API endpoints
- Static file serving and template rendering
The app package is structured as follows:
- core/: Core business logic and unit file handling
- api/: API route handlers (if separated from main.py)
- main.py: FastAPI application entry point
"""
__version__ = "1.1.0"
from .core.templates import template_registry
# Core exports for easy importing
from .core.unit_file import SystemdUnitFile, UnitType, ValidationError, create_unit_file
__all__ = [
"SystemdUnitFile",
"UnitType",
"ValidationError",
"create_unit_file",
"template_registry",
]

View File

@@ -0,0 +1,67 @@
"""
UnitForge Core Package
This package contains the core functionality for systemd unit file management:
- unit_file.py: SystemdUnitFile class for parsing, validating, and generating unit files
- templates.py: Template system for common systemd unit configurations
The core package provides the fundamental building blocks that can be used
independently or as part of the larger UnitForge application.
Key Components:
- SystemdUnitFile: Main class for unit file operations
- UnitType: Enumeration of supported unit types
- ValidationError: Exception class for validation errors
- create_unit_file: Factory function for creating unit files
- template_registry: Registry of available templates
- Template classes: Pre-built templates for common use cases
This package is designed to be:
- Self-contained with minimal external dependencies
- Well-tested and reliable
- Easy to use programmatically
- Extensible for new unit types and templates
"""
__version__ = "1.1.0"
from .templates import (
BackupTimerTemplate,
ContainerTemplate,
DatabaseTemplate,
ProxySocketTemplate,
TemplateParameter,
TemplateRegistry,
UnitTemplate,
WebApplicationTemplate,
template_registry,
)
# Core exports
from .unit_file import (
SystemdUnitFile,
UnitFileInfo,
UnitType,
ValidationError,
create_unit_file,
)
__all__ = [
# Unit file functionality
"SystemdUnitFile",
"UnitType",
"UnitFileInfo",
"ValidationError",
"create_unit_file",
# Template system
"template_registry",
"UnitTemplate",
"TemplateParameter",
"TemplateRegistry",
"WebApplicationTemplate",
"DatabaseTemplate",
"BackupTimerTemplate",
"ProxySocketTemplate",
"ContainerTemplate",
]

View 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()

View File

@@ -0,0 +1,879 @@
"""
Core systemd unit file parser, validator, and generator.
This module provides the fundamental functionality for working with systemd unit files,
including parsing, validation, and generation.
"""
import configparser
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
class UnitType(Enum):
SERVICE = "service"
TIMER = "timer"
SOCKET = "socket"
MOUNT = "mount"
TARGET = "target"
PATH = "path"
SLICE = "slice"
@dataclass
class ValidationError:
"""Represents a validation error in a unit file."""
section: str
key: Optional[str]
message: str
severity: str = "error" # error, warning, info
line_number: Optional[int] = None
@dataclass
class UnitFileInfo:
"""Metadata about a unit file."""
name: str
unit_type: UnitType
description: Optional[str] = None
documentation: List[str] = field(default_factory=list)
requires: List[str] = field(default_factory=list)
wants: List[str] = field(default_factory=list)
conflicts: List[str] = field(default_factory=list)
class SystemdUnitFile:
"""
Parser and validator for systemd unit files.
This class can parse existing unit files, validate their syntax and semantics,
and generate new unit files from structured data.
"""
# Common systemd unit file sections
COMMON_SECTIONS = {
"Unit",
"Install",
"Service",
"Timer",
"Socket",
"Mount",
"Target",
"Path",
"Slice",
}
# Valid keys for each section
SECTION_KEYS = {
"Unit": {
"Description",
"Documentation",
"Requires",
"Wants",
"After",
"Before",
"Conflicts",
"ConditionPathExists",
"ConditionFileNotEmpty",
"AssertPathExists",
"OnFailure",
"StopWhenUnneeded",
"RefuseManualStart",
"RefuseManualStop",
"AllowIsolate",
"DefaultDependencies",
"JobTimeoutSec",
"StartLimitInterval",
"StartLimitBurst",
"RebootArgument",
"ConditionArchitecture",
"ConditionVirtualization",
"ConditionHost",
"ConditionKernelCommandLine",
"ConditionSecurity",
"ConditionCapability",
"ConditionACPower",
"ConditionNeedsUpdate",
"ConditionFirstBoot",
"ConditionPathIsDirectory",
"ConditionPathIsSymbolicLink",
"ConditionPathIsMountPoint",
"ConditionPathIsReadWrite",
"ConditionDirectoryNotEmpty",
"ConditionFileIsExecutable",
"ConditionUser",
"ConditionGroup",
},
"Install": {"Alias", "WantedBy", "RequiredBy", "Also", "DefaultInstance"},
"Service": {
"Type",
"ExecStart",
"ExecStartPre",
"ExecStartPost",
"ExecReload",
"ExecStop",
"ExecStopPost",
"RestartSec",
"TimeoutStartSec",
"TimeoutStopSec",
"TimeoutSec",
"RuntimeMaxSec",
"WatchdogSec",
"Restart",
"SuccessExitStatus",
"RestartPreventExitStatus",
"RestartForceExitStatus",
"PermissionsStartOnly",
"RootDirectoryStartOnly",
"RemainAfterExit",
"GuessMainPID",
"PIDFile",
"BusName",
"BusPolicy",
"User",
"Group",
"SupplementaryGroups",
"WorkingDirectory",
"RootDirectory",
"Nice",
"OOMScoreAdjust",
"IOSchedulingClass",
"IOSchedulingPriority",
"CPUSchedulingPolicy",
"CPUSchedulingPriority",
"CPUSchedulingResetOnFork",
"CPUAffinity",
"UMask",
"Environment",
"EnvironmentFile",
"PassEnvironment",
"UnsetEnvironment",
"StandardInput",
"StandardOutput",
"StandardError",
"TTYPath",
"TTYReset",
"TTYVHangup",
"TTYVTDisallocate",
"SyslogIdentifier",
"SyslogFacility",
"SyslogLevel",
"SyslogLevelPrefix",
"TimerSlackNSec",
"LimitCPU",
"LimitFSIZE",
"LimitDATA",
"LimitSTACK",
"LimitCORE",
"LimitRSS",
"LimitNOFILE",
"LimitAS",
"LimitNPROC",
"LimitMEMLOCK",
"LimitLOCKS",
"LimitSIGPENDING",
"LimitMSGQUEUE",
"LimitNICE",
"LimitRTPRIO",
"LimitRTTIME",
"PAMName",
"CapabilityBoundingSet",
"AmbientCapabilities",
"SecureBits",
"ReadWritePaths",
"ReadOnlyPaths",
"InaccessiblePaths",
"PrivateTmp",
"PrivateDevices",
"PrivateNetwork",
"ProtectSystem",
"ProtectHome",
"MountFlags",
"UtmpIdentifier",
"UtmpMode",
"SELinuxContext",
"AppArmorProfile",
"SmackProcessLabel",
"IgnoreSIGPIPE",
"NoNewPrivileges",
"SystemCallFilter",
"SystemCallErrorNumber",
"SystemCallArchitectures",
"RestrictAddressFamilies",
"Personality",
"RuntimeDirectory",
"RuntimeDirectoryMode",
},
"Timer": {
"OnActiveSec",
"OnBootSec",
"OnStartupSec",
"OnUnitActiveSec",
"OnUnitInactiveSec",
"OnCalendar",
"AccuracySec",
"RandomizedDelaySec",
"Unit",
"Persistent",
"WakeSystem",
"RemainAfterElapse",
},
"Socket": {
"ListenStream",
"ListenDatagram",
"ListenSequentialPacket",
"ListenFIFO",
"ListenSpecial",
"ListenNetlink",
"ListenMessageQueue",
"ListenUSBFunction",
"SocketProtocol",
"BindIPv6Only",
"Backlog",
"BindToDevice",
"SocketUser",
"SocketGroup",
"SocketMode",
"DirectoryMode",
"Accept",
"Writable",
"MaxConnections",
"MaxConnectionsPerSource",
"KeepAlive",
"KeepAliveTimeSec",
"KeepAliveIntervalSec",
"KeepAliveProbes",
"NoDelay",
"Priority",
"DeferAcceptSec",
"ReceiveBuffer",
"SendBuffer",
"IPTOS",
"IPTTL",
"Mark",
"ReusePort",
"SmackLabel",
"SmackLabelIPIn",
"SmackLabelIPOut",
"SELinuxContextFromNet",
"PipeSize",
"MessageQueueMaxMessages",
"MessageQueueMessageSize",
"FreeBind",
"Transparent",
"Broadcast",
"PassCredentials",
"PassSecurity",
"TCPCongestion",
"ExecStartPre",
"ExecStartPost",
"ExecStopPre",
"ExecStopPost",
"TimeoutSec",
"Service",
"RemoveOnStop",
"Symlinks",
"FileDescriptorName",
"TriggerLimitIntervalSec",
"TriggerLimitBurst",
},
"Mount": {
"What",
"Where",
"Type",
"Options",
"SloppyOptions",
"LazyUnmount",
"ForceUnmount",
"DirectoryMode",
"TimeoutSec",
},
"Target": {},
"Path": {
"PathExists",
"PathExistsGlob",
"PathChanged",
"PathModified",
"DirectoryNotEmpty",
"Unit",
"MakeDirectory",
"DirectoryMode",
},
}
# Service types and their requirements
SERVICE_TYPES = {
"simple": {"required": ["ExecStart"], "conflicts": ["BusName", "Type=forking"]},
"exec": {"required": ["ExecStart"], "conflicts": ["BusName"]},
"forking": {"recommended": ["PIDFile"], "conflicts": ["BusName"]},
"oneshot": {"conflicts": ["Restart=always", "Restart=on-success"]},
"dbus": {"required": ["BusName"], "conflicts": []},
"notify": {"required": ["ExecStart"], "conflicts": ["BusName"]},
"idle": {"required": ["ExecStart"], "conflicts": ["BusName"]},
}
def __init__(self, content: Optional[str] = None, file_path: Optional[str] = None):
"""Initialize with either content string or file path."""
self.content = content or ""
self.file_path = file_path
self.config = configparser.ConfigParser(
interpolation=None, allow_no_value=True, delimiters=("=",)
)
self.config.optionxform = lambda optionstr: str(optionstr) # Preserve case
self._parse_errors = []
if content:
self._parse_content(content)
elif file_path:
self._parse_file(file_path)
def _parse_content(self, content: str) -> None:
"""Parse unit file content from string."""
try:
self.config.read_string(content)
self.content = content
except configparser.Error as e:
self._parse_errors.append(
ValidationError("", None, f"Parse error: {str(e)}", "error")
)
def _parse_file(self, file_path: str) -> None:
"""Parse unit file from file path."""
try:
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
self._parse_content(content)
self.file_path = file_path
except IOError as e:
self._parse_errors.append(
ValidationError("", None, f"File read error: {str(e)}", "error")
)
def get_unit_type(self) -> Optional[UnitType]:
"""Determine the unit type based on sections present."""
if self.config.has_section("Service"):
return UnitType.SERVICE
elif self.config.has_section("Timer"):
return UnitType.TIMER
elif self.config.has_section("Socket"):
return UnitType.SOCKET
elif self.config.has_section("Mount"):
return UnitType.MOUNT
elif self.config.has_section("Target"):
return UnitType.TARGET
elif self.config.has_section("Path"):
return UnitType.PATH
return None
def get_info(self) -> UnitFileInfo:
"""Extract basic information about the unit file."""
unit_type = self.get_unit_type()
name = ""
if self.file_path:
name = self.file_path.split("/")[-1]
description = None
if self.config.has_section("Unit") and self.config.has_option(
"Unit", "Description"
):
description = self.config.get("Unit", "Description")
documentation = []
if self.config.has_section("Unit") and self.config.has_option(
"Unit", "Documentation"
):
doc_value = self.config.get("Unit", "Documentation")
documentation = [doc.strip() for doc in doc_value.split()]
# Parse dependencies
requires = self._get_dependency_list("Requires")
wants = self._get_dependency_list("Wants")
conflicts = self._get_dependency_list("Conflicts")
if unit_type is None:
raise ValueError("Could not determine unit type")
return UnitFileInfo(
name=name,
unit_type=unit_type,
description=description,
documentation=documentation,
requires=requires,
wants=wants,
conflicts=conflicts,
)
def _get_dependency_list(self, key: str) -> List[str]:
"""Extract a space-separated dependency list."""
if not self.config.has_section("Unit") or not self.config.has_option(
"Unit", key
):
return []
value = self.config.get("Unit", key)
return [dep.strip() for dep in value.split() if dep.strip()]
def validate(self) -> List[ValidationError]:
"""Validate the unit file and return list of errors/warnings."""
errors = self._parse_errors.copy()
# Check for basic structure
if not self.config.sections():
errors.append(
ValidationError(
"", None, "Unit file is empty or has no sections", "error"
)
)
return errors
# Validate sections
for section in self.config.sections():
errors.extend(self._validate_section(section))
# Type-specific validation
unit_type = self.get_unit_type()
if unit_type:
errors.extend(self._validate_unit_type(unit_type))
return errors
def _validate_section(self, section: str) -> List[ValidationError]:
"""Validate a specific section."""
errors = []
# Check if section is known
if section not in self.COMMON_SECTIONS:
errors.append(
ValidationError(
section, None, f"Unknown section '{section}'", "warning"
)
)
return errors
# Validate keys in section
valid_keys = self.SECTION_KEYS.get(section, set())
for key in self.config.options(section):
if valid_keys and key not in valid_keys:
errors.append(
ValidationError(
section,
key,
f"Unknown key '{key}' in section '{section}'",
"warning",
)
)
# Validate key values
value = self.config.get(section, key)
errors.extend(self._validate_key_value(section, key, value))
return errors
def _validate_key_value(
self, section: str, key: str, value: str
) -> List[ValidationError]:
"""Validate specific key-value pairs."""
errors = []
# Service-specific validations
if section == "Service":
if key == "Type" and value not in self.SERVICE_TYPES:
errors.append(
ValidationError(
section, key, f"Invalid service type '{value}'", "error"
)
)
elif key == "Restart" and value not in [
"no",
"always",
"on-success",
"on-failure",
"on-abnormal",
"on-abort",
"on-watchdog",
]:
errors.append(
ValidationError(
section, key, f"Invalid restart value '{value}'", "error"
)
)
elif key.startswith("Exec") and not value:
errors.append(
ValidationError(
section, key, f"Empty execution command for '{key}'", "error"
)
)
elif key in ["TimeoutStartSec", "TimeoutStopSec", "RestartSec"] and value:
if not self._is_valid_time_span(value):
errors.append(
ValidationError(
section, key, f"Invalid time span '{value}'", "error"
)
)
# Timer-specific validations
elif section == "Timer":
if key.startswith("On") and key.endswith("Sec") and value:
if not self._is_valid_time_span(value):
errors.append(
ValidationError(
section, key, f"Invalid time span '{value}'", "error"
)
)
elif key == "OnCalendar" and value:
# Basic calendar validation - could be more comprehensive
if not re.match(r"^[\w\s:*/-]+$", value):
errors.append(
ValidationError(
section,
key,
f"Invalid calendar specification '{value}'",
"warning",
)
)
# Socket-specific validations
elif section == "Socket":
if key.startswith("Listen") and not value:
errors.append(
ValidationError(
section, key, f"Empty listen specification for '{key}'", "error"
)
)
# Mount-specific validations
elif section == "Mount":
if key == "What" and not value:
errors.append(
ValidationError(
section, key, "Mount source (What) cannot be empty", "error"
)
)
elif key == "Where" and not value:
errors.append(
ValidationError(
section, key, "Mount point (Where) cannot be empty", "error"
)
)
elif key == "Where" and not value.startswith("/"):
errors.append(
ValidationError(
section, key, "Mount point must be an absolute path", "error"
)
)
return errors
def _validate_unit_type(self, unit_type: UnitType) -> List[ValidationError]:
"""Perform type-specific validation."""
errors = []
if unit_type == UnitType.SERVICE:
errors.extend(self._validate_service())
elif unit_type == UnitType.TIMER:
errors.extend(self._validate_timer())
elif unit_type == UnitType.SOCKET:
errors.extend(self._validate_socket())
elif unit_type == UnitType.MOUNT:
errors.extend(self._validate_mount())
return errors
def _validate_service(self) -> List[ValidationError]:
"""Validate service-specific requirements."""
errors = []
if not self.config.has_section("Service"):
return errors
service_type = self.config.get("Service", "Type", fallback="simple")
type_config = self.SERVICE_TYPES.get(service_type, {})
# Check required keys
for required_key in type_config.get("required", []):
if not self.config.has_option("Service", required_key):
errors.append(
ValidationError(
"Service",
required_key,
(
f"Required key '{required_key}' missing for "
f"service type '{service_type}'"
),
"error",
)
)
# Check for conflicting configurations
for conflict in type_config.get("conflicts", []):
if "=" in conflict:
key, value = conflict.split("=", 1)
if self.config.has_option("Service", key):
actual_value = self.config.get("Service", key)
if actual_value == value:
errors.append(
ValidationError(
"Service",
key,
(
f"'{key}={value}' conflicts with "
f"service type '{service_type}'"
),
"error",
)
)
else:
if self.config.has_option("Service", conflict):
errors.append(
ValidationError(
"Service",
conflict,
(
f"'{conflict}' conflicts with "
f"service type '{service_type}'"
),
"error",
)
)
return errors
def _validate_timer(self) -> List[ValidationError]:
"""Validate timer-specific requirements."""
errors = []
if not self.config.has_section("Timer"):
return errors
# At least one timer specification is required
timer_keys = [
"OnActiveSec",
"OnBootSec",
"OnStartupSec",
"OnUnitActiveSec",
"OnUnitInactiveSec",
"OnCalendar",
]
has_timer = any(self.config.has_option("Timer", key) for key in timer_keys)
if not has_timer:
errors.append(
ValidationError(
"Timer",
None,
"Timer must have at least one timing specification",
"error",
)
)
return errors
def _validate_socket(self) -> List[ValidationError]:
"""Validate socket-specific requirements."""
errors = []
if not self.config.has_section("Socket"):
return errors
# At least one listen specification is required
listen_keys = [
"ListenStream",
"ListenDatagram",
"ListenSequentialPacket",
"ListenFIFO",
"ListenSpecial",
"ListenNetlink",
"ListenMessageQueue",
]
has_listen = any(self.config.has_option("Socket", key) for key in listen_keys)
if not has_listen:
errors.append(
ValidationError(
"Socket",
None,
"Socket must have at least one Listen specification",
"error",
)
)
return errors
def _validate_mount(self) -> List[ValidationError]:
"""Validate mount-specific requirements."""
errors = []
if not self.config.has_section("Mount"):
return errors
# What and Where are required
if not self.config.has_option("Mount", "What"):
errors.append(
ValidationError(
"Mount",
"What",
"Mount units require 'What' (source) specification",
"error",
)
)
if not self.config.has_option("Mount", "Where"):
errors.append(
ValidationError(
"Mount",
"Where",
"Mount units require 'Where' (mount point) specification",
"error",
)
)
return errors
def _is_valid_time_span(self, value: str) -> bool:
"""Validate systemd time span format."""
# Basic validation for time spans like "30s", "5min", "1h", "infinity"
if value.lower() in ["infinity", "0"]:
return True
pattern = r"^\d+(\.\d+)?(us|ms|s|min|h|d|w|month|y)?$"
return bool(re.match(pattern, value.lower()))
def to_string(self) -> str:
"""Convert the unit file back to string format."""
if not self.config.sections():
return ""
lines = []
for section in self.config.sections():
lines.append(f"[{section}]")
for key in self.config.options(section):
value = self.config.get(section, key)
if value is None:
lines.append(key)
else:
lines.append(f"{key}={value}")
lines.append("") # Empty line between sections
return "\n".join(lines).rstrip()
def set_value(self, section: str, key: str, value: str) -> None:
"""Set a value in the unit file."""
if not self.config.has_section(section):
self.config.add_section(section)
self.config.set(section, key, value)
def get_value(
self, section: str, key: str, fallback: Optional[str] = None
) -> Optional[str]:
"""Get a value from the unit file."""
if not self.config.has_section(section):
return fallback
return self.config.get(section, key, fallback=fallback)
def remove_key(self, section: str, key: str) -> bool:
"""Remove a key from the unit file."""
if self.config.has_section(section) and self.config.has_option(section, key):
self.config.remove_option(section, key)
return True
return False
def get_sections(self) -> List[str]:
"""Get all sections in the unit file."""
return self.config.sections()
def get_keys(self, section: str) -> List[str]:
"""Get all keys in a section."""
if not self.config.has_section(section):
return []
return self.config.options(section)
def create_unit_file(unit_type: UnitType, **kwargs) -> SystemdUnitFile:
"""
Create a new unit file of the specified type with basic structure.
Args:
unit_type: The type of unit file to create
**kwargs: Additional configuration parameters
Returns:
SystemdUnitFile instance with basic structure
"""
unit = SystemdUnitFile()
# Add Unit section
unit.set_value(
"Unit",
"Description",
kwargs.get("description", f"Generated {unit_type.value} unit"),
)
if kwargs.get("documentation"):
unit.set_value("Unit", "Documentation", kwargs["documentation"])
if kwargs.get("requires"):
unit.set_value("Unit", "Requires", " ".join(kwargs["requires"]))
if kwargs.get("wants"):
unit.set_value("Unit", "Wants", " ".join(kwargs["wants"]))
if kwargs.get("after"):
unit.set_value("Unit", "After", " ".join(kwargs["after"]))
# Add type-specific sections
if unit_type == UnitType.SERVICE:
unit.set_value("Service", "Type", kwargs.get("service_type", "simple"))
if kwargs.get("exec_start"):
unit.set_value("Service", "ExecStart", kwargs["exec_start"])
if kwargs.get("user"):
unit.set_value("Service", "User", kwargs["user"])
if kwargs.get("group"):
unit.set_value("Service", "Group", kwargs["group"])
if kwargs.get("working_directory"):
unit.set_value("Service", "WorkingDirectory", kwargs["working_directory"])
unit.set_value("Service", "Restart", kwargs.get("restart", "on-failure"))
elif unit_type == UnitType.TIMER:
if kwargs.get("on_calendar"):
unit.set_value("Timer", "OnCalendar", kwargs["on_calendar"])
if kwargs.get("on_boot_sec"):
unit.set_value("Timer", "OnBootSec", kwargs["on_boot_sec"])
unit.set_value(
"Timer", "Persistent", str(kwargs.get("persistent", "true")).lower()
)
elif unit_type == UnitType.SOCKET:
if kwargs.get("listen_stream"):
unit.set_value("Socket", "ListenStream", kwargs["listen_stream"])
if kwargs.get("listen_datagram"):
unit.set_value("Socket", "ListenDatagram", kwargs["listen_datagram"])
elif unit_type == UnitType.MOUNT:
if kwargs.get("what"):
unit.set_value("Mount", "What", kwargs["what"])
if kwargs.get("where"):
unit.set_value("Mount", "Where", kwargs["where"])
if kwargs.get("type"):
unit.set_value("Mount", "Type", kwargs["type"])
if kwargs.get("options"):
unit.set_value("Mount", "Options", kwargs["options"])
# Add Install section if specified
if kwargs.get("wanted_by"):
wanted_by_value = kwargs["wanted_by"]
if isinstance(wanted_by_value, (list, tuple)):
unit.set_value("Install", "WantedBy", " ".join(wanted_by_value))
else:
unit.set_value("Install", "WantedBy", str(wanted_by_value))
elif unit_type in [UnitType.SERVICE, UnitType.TIMER]:
unit.set_value("Install", "WantedBy", "multi-user.target")
return unit

407
backend/app/main.py Normal file
View File

@@ -0,0 +1,407 @@
"""
Main FastAPI application for UnitForge.
This module provides the web API for creating, validating, and managing
systemd unit files through HTTP endpoints.
"""
import tempfile
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import FastAPI, File, HTTPException, Request, UploadFile # type: ignore
from fastapi.middleware.cors import CORSMiddleware # type: ignore
from fastapi.responses import FileResponse, HTMLResponse # type: ignore
from fastapi.staticfiles import StaticFiles # type: ignore
from fastapi.templating import Jinja2Templates # type: ignore
from pydantic import BaseModel
from .core.templates import UnitTemplate, template_registry
from .core.unit_file import SystemdUnitFile, UnitType, ValidationError, create_unit_file
# Create FastAPI app
app = FastAPI(
title="UnitForge",
description="Create, validate, and manage systemd unit files",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Setup templates and static files
BASE_DIR = Path(__file__).resolve().parent.parent.parent
TEMPLATES_DIR = BASE_DIR / "frontend" / "templates"
STATIC_DIR = BASE_DIR / "frontend" / "static"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
# Mount static files
if STATIC_DIR.exists():
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
# Pydantic models for API
class ValidationResult(BaseModel):
"""Result of unit file validation."""
valid: bool
errors: List[Dict[str, Any]]
warnings: List[Dict[str, Any]]
class UnitFileContent(BaseModel):
"""Unit file content for validation/generation."""
content: str
filename: Optional[str] = None
class GenerateRequest(BaseModel):
"""Request to generate a unit file from parameters."""
template_name: str
parameters: Dict[str, Any]
filename: Optional[str] = None
class TemplateInfo(BaseModel):
"""Template information."""
name: str
description: str
unit_type: str
category: str
parameters: List[Dict[str, Any]]
tags: List[str]
class CreateUnitRequest(BaseModel):
"""Request to create a basic unit file."""
unit_type: str
name: str
description: Optional[str] = None
parameters: Dict[str, Any] = {}
# Helper functions
def validation_error_to_dict(error: ValidationError) -> Dict[str, Any]:
"""Convert ValidationError to dictionary."""
return {
"section": error.section,
"key": error.key,
"message": error.message,
"severity": error.severity,
"line_number": error.line_number,
}
def template_to_dict(template: UnitTemplate) -> Dict[str, Any]:
"""Convert UnitTemplate to dictionary."""
parameters = []
for param in template.parameters:
param_dict = {
"name": param.name,
"description": param.description,
"type": param.parameter_type,
"required": param.required,
"default": param.default,
"choices": param.choices,
"example": param.example,
}
parameters.append(param_dict)
return {
"name": template.name,
"description": template.description,
"unit_type": template.unit_type.value,
"category": template.category,
"parameters": parameters,
"tags": template.tags or [],
}
# Web UI Routes
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
"""Serve the main web interface."""
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/editor", response_class=HTMLResponse)
async def editor(request: Request):
"""Serve the unit file editor interface."""
return templates.TemplateResponse("editor.html", {"request": request})
@app.get("/templates", response_class=HTMLResponse)
async def templates_page(request: Request):
"""Serve the templates browser interface."""
return templates.TemplateResponse("templates.html", {"request": request})
# API Routes
@app.post("/api/validate", response_model=ValidationResult)
async def validate_unit_file(unit_file: UnitFileContent):
"""Validate a systemd unit file."""
try:
systemd_unit = SystemdUnitFile(content=unit_file.content)
validation_errors = systemd_unit.validate()
errors = []
warnings = []
for error in validation_errors:
error_dict = validation_error_to_dict(error)
if error.severity == "error":
errors.append(error_dict)
else:
warnings.append(error_dict)
return ValidationResult(
valid=len(errors) == 0, errors=errors, warnings=warnings
)
except Exception as e:
raise HTTPException(status_code=400, detail=f"Validation failed: {str(e)}")
@app.post("/api/generate")
async def generate_unit_file(request: GenerateRequest):
"""Generate a unit file from a template."""
try:
template = template_registry.get_template(request.template_name)
if not template:
raise HTTPException(
status_code=404, detail=f"Template '{request.template_name}' not found"
)
# Validate required parameters
for param in template.parameters:
if param.required and param.name not in request.parameters:
raise HTTPException(
status_code=400,
detail=f"Required parameter '{param.name}' is missing",
)
# Generate the unit file
unit_file = template.generate(request.parameters)
content = unit_file.to_string()
# Validate the generated content
validation_errors = unit_file.validate()
errors = [
validation_error_to_dict(e)
for e in validation_errors
if e.severity == "error"
]
warnings = [
validation_error_to_dict(e)
for e in validation_errors
if e.severity != "error"
]
return {
"content": content,
"filename": (
request.filename
or f"{request.parameters.get('name', 'service')}"
f".{template.unit_type.value}"
),
"validation": {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Generation failed: {str(e)}")
@app.post("/api/create")
async def create_unit_file_endpoint(request: CreateUnitRequest):
"""Create a basic unit file."""
try:
# Parse unit type
try:
unit_type = UnitType(request.unit_type.lower())
except ValueError:
raise HTTPException(
status_code=400, detail=f"Invalid unit type: {request.unit_type}"
)
# Create basic unit file
kwargs = {
"description": request.description or f"{request.name} {unit_type.value}",
**request.parameters,
}
unit_file = create_unit_file(unit_type, **kwargs)
content = unit_file.to_string()
# Validate the created content
validation_errors = unit_file.validate()
errors = [
validation_error_to_dict(e)
for e in validation_errors
if e.severity == "error"
]
warnings = [
validation_error_to_dict(e)
for e in validation_errors
if e.severity != "error"
]
return {
"content": content,
"filename": f"{request.name}.{unit_type.value}",
"validation": {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Creation failed: {str(e)}")
@app.get("/api/templates", response_model=List[TemplateInfo])
async def list_templates():
"""List all available templates."""
templates = template_registry.list_templates()
return [TemplateInfo(**template_to_dict(template)) for template in templates]
@app.get("/api/templates/{template_name}", response_model=TemplateInfo)
async def get_template(template_name: str):
"""Get details of a specific template."""
template = template_registry.get_template(template_name)
if not template:
raise HTTPException(
status_code=404, detail=f"Template '{template_name}' not found"
)
return TemplateInfo(**template_to_dict(template))
@app.get("/api/templates/category/{category}", response_model=List[TemplateInfo])
async def get_templates_by_category(category: str):
"""Get templates by category."""
templates = template_registry.get_by_category(category)
return [TemplateInfo(**template_to_dict(template)) for template in templates]
@app.get("/api/templates/type/{unit_type}", response_model=List[TemplateInfo])
async def get_templates_by_type(unit_type: str):
"""Get templates by unit type."""
try:
unit_type_enum = UnitType(unit_type.lower())
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid unit type: {unit_type}")
templates = template_registry.get_by_type(unit_type_enum)
return [TemplateInfo(**template_to_dict(template)) for template in templates]
@app.get("/api/search/{query}", response_model=List[TemplateInfo])
async def search_templates(query: str):
"""Search templates by name, description, or tags."""
templates = template_registry.search(query)
return [TemplateInfo(**template_to_dict(template)) for template in templates]
@app.post("/api/download")
async def download_unit_file(unit_file: UnitFileContent):
"""Download a unit file."""
try:
# Create a temporary file
filename = unit_file.filename or "unit.service"
temp_file = tempfile.NamedTemporaryFile(
mode="w", delete=False, suffix=f"_{filename}"
)
temp_file.write(unit_file.content)
temp_file.close()
return FileResponse(
path=temp_file.name,
filename=filename,
media_type="application/octet-stream",
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Download failed: {str(e)}")
@app.post("/api/upload")
async def upload_unit_file(file: UploadFile = File(...)):
"""Upload and validate a unit file."""
try:
content = await file.read()
content_str = content.decode("utf-8")
# Parse and validate the uploaded file
systemd_unit = SystemdUnitFile(content=content_str)
validation_errors = systemd_unit.validate()
errors = []
warnings = []
for error in validation_errors:
error_dict = validation_error_to_dict(error)
if error.severity == "error":
errors.append(error_dict)
else:
warnings.append(error_dict)
# Get unit info
unit_info = systemd_unit.get_info()
return {
"filename": file.filename,
"content": content_str,
"unit_type": unit_info.unit_type.value if unit_info.unit_type else None,
"description": unit_info.description,
"validation": {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
},
}
except UnicodeDecodeError:
raise HTTPException(status_code=400, detail="File must be valid UTF-8 text")
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Upload processing failed: {str(e)}"
)
@app.get("/api/info")
async def get_info():
"""Get application information."""
return {
"name": "UnitForge",
"version": "1.0.0",
"description": "Create, validate, and manage systemd unit files",
"supported_types": [t.value for t in UnitType],
"template_count": len(template_registry.list_templates()),
}
# Health check
@app.get("/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "service": "unitforge"}
if __name__ == "__main__":
import uvicorn # type: ignore
uvicorn.run(app, host="0.0.0.0", port=8000) # nosec B104