wrolpi/controller/lib/systemd.py

322 lines
8.6 KiB
Python

"""
Systemd service management for WROLPi Controller.
Provides start/stop/restart/status/logs for systemd services.
"""
import subprocess
from typing import Optional
from controller.lib.config import get_config
def get_managed_services() -> list[dict]:
"""
Get list of managed services from config.
Returns:
list of service configs with name, systemd_name, port, etc.
"""
config = get_config()
return config.get("managed_services", [])
def get_service_config(name: str) -> Optional[dict]:
"""
Get config for a specific service by name.
Args:
name: Service name (e.g., "wrolpi-api")
Returns:
Service config dict or None if not found
"""
for service in get_managed_services():
if service.get("name") == name:
return service
return None
def _run_systemctl(command: str, service: str, timeout: int = 30) -> dict:
"""
Run a systemctl command.
Args:
command: systemctl command (start, stop, restart, status, etc.)
service: systemd service name
timeout: Command timeout in seconds
Returns:
dict with success, output, and error fields
"""
try:
result = subprocess.run(
["systemctl", command, service],
capture_output=True,
text=True,
timeout=timeout,
)
return {
"success": result.returncode == 0,
"output": result.stdout.strip(),
"error": result.stderr.strip() if result.returncode != 0 else None,
}
except subprocess.TimeoutExpired:
return {"success": False, "output": "", "error": "Command timed out"}
except FileNotFoundError:
return {"success": False, "output": "", "error": "systemctl not found"}
def get_service_status(name: str) -> dict:
"""
Get status of a managed service.
Args:
name: Service name from config
Returns:
dict with status, active, enabled, etc.
"""
service_config = get_service_config(name)
if not service_config:
return {"error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
try:
# Get active state
active_result = subprocess.run(
["systemctl", "is-active", systemd_name],
capture_output=True,
text=True,
timeout=5,
)
is_active = active_result.stdout.strip()
# Get enabled state
enabled_result = subprocess.run(
["systemctl", "is-enabled", systemd_name],
capture_output=True,
text=True,
timeout=5,
)
is_enabled = enabled_result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return {
"name": name,
"systemd_name": systemd_name,
"status": "unknown",
"active": "unknown",
"enabled": False,
"port": service_config.get("port"),
"viewable": service_config.get("viewable", False),
"view_path": service_config.get("view_path", ""),
"use_https": service_config.get("use_https", False),
"description": service_config.get("description", ""),
"error": "systemctl not available",
}
# Map to simple status
if is_active == "active":
status = "running"
elif is_active == "activating":
status = "running" # oneshot services show "activating" while running
elif is_active == "inactive":
status = "stopped"
elif is_active == "failed":
status = "failed"
else:
status = "unknown"
return {
"name": name,
"systemd_name": systemd_name,
"status": status,
"active": is_active,
"enabled": is_enabled == "enabled",
"port": service_config.get("port"),
"viewable": service_config.get("viewable", False),
"view_path": service_config.get("view_path", ""),
"use_https": service_config.get("use_https", False),
"description": service_config.get("description", ""),
}
def get_all_services_status() -> list[dict]:
"""
Get status of all managed services.
Services with show_only_when_running=True are excluded when not running.
Returns:
list of service status dicts
"""
results = []
for service in get_managed_services():
status = get_service_status(service["name"])
# Skip services with show_only_when_running if not running
if service.get("show_only_when_running") and status.get("status") != "running":
continue
results.append(status)
return results
def start_service(name: str) -> dict:
"""
Start a managed service.
Args:
name: Service name from config
Returns:
dict with success status
"""
service_config = get_service_config(name)
if not service_config:
return {"success": False, "error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
result = _run_systemctl("start", systemd_name)
return {
"success": result["success"],
"service": name,
"action": "start",
"error": result.get("error"),
}
def stop_service(name: str) -> dict:
"""
Stop a managed service.
Args:
name: Service name from config
Returns:
dict with success status
"""
service_config = get_service_config(name)
if not service_config:
return {"success": False, "error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
result = _run_systemctl("stop", systemd_name)
return {
"success": result["success"],
"service": name,
"action": "stop",
"error": result.get("error"),
}
def restart_service(name: str) -> dict:
"""
Restart a managed service.
Args:
name: Service name from config
Returns:
dict with success status
"""
service_config = get_service_config(name)
if not service_config:
return {"success": False, "error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
result = _run_systemctl("restart", systemd_name)
return {
"success": result["success"],
"service": name,
"action": "restart",
"error": result.get("error"),
}
def enable_service(name: str) -> dict:
"""
Enable a service to start at boot.
Args:
name: Service name from config
Returns:
dict with success status
"""
service_config = get_service_config(name)
if not service_config:
return {"success": False, "error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
result = _run_systemctl("enable", systemd_name)
return {
"success": result["success"],
"service": name,
"action": "enable",
"error": result.get("error"),
}
def disable_service(name: str) -> dict:
"""
Disable a service from starting at boot.
Args:
name: Service name from config
Returns:
dict with success status
"""
service_config = get_service_config(name)
if not service_config:
return {"success": False, "error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
result = _run_systemctl("disable", systemd_name)
return {
"success": result["success"],
"service": name,
"action": "disable",
"error": result.get("error"),
}
def get_service_logs(name: str, lines: int = 100, since: Optional[str] = None) -> dict:
"""
Get logs for a service.
Args:
name: Service name from config
lines: Number of lines to return
since: Time specification (e.g., "1h", "30m", "2024-01-01")
Returns:
dict with logs and metadata
"""
service_config = get_service_config(name)
if not service_config:
return {"error": f"Unknown service: {name}"}
systemd_name = service_config.get("systemd_name", name)
cmd = ["journalctl", "-u", systemd_name, "-n", str(lines), "--no-pager"]
if since:
cmd.extend(["--since", since])
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
return {
"service": name,
"lines": lines,
"since": since,
"logs": result.stdout,
}
except subprocess.TimeoutExpired:
return {"error": "Timeout getting logs"}
except FileNotFoundError:
return {"error": "journalctl not found"}