mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 19:16:58 +00:00
375 lines
12 KiB
Go
375 lines
12 KiB
Go
package systemd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ServiceType represents the type of namespace service
|
|
type ServiceType string
|
|
|
|
const (
|
|
ServiceTypeRQLite ServiceType = "rqlite"
|
|
ServiceTypeOlric ServiceType = "olric"
|
|
ServiceTypeGateway ServiceType = "gateway"
|
|
)
|
|
|
|
// Manager manages systemd units for namespace services
|
|
type Manager struct {
|
|
logger *zap.Logger
|
|
systemdDir string
|
|
namespaceBase string // Base directory for namespace data
|
|
}
|
|
|
|
// NewManager creates a new systemd manager
|
|
func NewManager(namespaceBase string, logger *zap.Logger) *Manager {
|
|
return &Manager{
|
|
logger: logger.With(zap.String("component", "systemd-manager")),
|
|
systemdDir: "/etc/systemd/system",
|
|
namespaceBase: namespaceBase,
|
|
}
|
|
}
|
|
|
|
// serviceName returns the systemd service name for a namespace and service type
|
|
func (m *Manager) serviceName(namespace string, serviceType ServiceType) string {
|
|
return fmt.Sprintf("debros-namespace-%s@%s.service", serviceType, namespace)
|
|
}
|
|
|
|
// StartService starts a namespace service
|
|
func (m *Manager) StartService(namespace string, serviceType ServiceType) error {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
m.logger.Info("Starting systemd service",
|
|
zap.String("service", svcName),
|
|
zap.String("namespace", namespace))
|
|
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "start", svcName)
|
|
m.logger.Debug("Executing systemctl command",
|
|
zap.String("cmd", cmd.String()),
|
|
zap.Strings("args", cmd.Args))
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
m.logger.Error("Failed to start service",
|
|
zap.String("service", svcName),
|
|
zap.Error(err),
|
|
zap.String("output", string(output)),
|
|
zap.String("cmd", cmd.String()))
|
|
return fmt.Errorf("failed to start %s: %w; output: %s", svcName, err, string(output))
|
|
}
|
|
|
|
m.logger.Info("Service started successfully",
|
|
zap.String("service", svcName),
|
|
zap.String("output", string(output)))
|
|
return nil
|
|
}
|
|
|
|
// StopService stops a namespace service
|
|
func (m *Manager) StopService(namespace string, serviceType ServiceType) error {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
m.logger.Info("Stopping systemd service",
|
|
zap.String("service", svcName),
|
|
zap.String("namespace", namespace))
|
|
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "stop", svcName)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
// Don't error if service is already stopped or doesn't exist
|
|
if strings.Contains(string(output), "not loaded") || strings.Contains(string(output), "inactive") {
|
|
m.logger.Debug("Service already stopped or not loaded", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to stop %s: %w; output: %s", svcName, err, string(output))
|
|
}
|
|
|
|
m.logger.Info("Service stopped successfully", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
|
|
// RestartService restarts a namespace service
|
|
func (m *Manager) RestartService(namespace string, serviceType ServiceType) error {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
m.logger.Info("Restarting systemd service",
|
|
zap.String("service", svcName),
|
|
zap.String("namespace", namespace))
|
|
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "restart", svcName)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to restart %s: %w; output: %s", svcName, err, string(output))
|
|
}
|
|
|
|
m.logger.Info("Service restarted successfully", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
|
|
// EnableService enables a namespace service to start on boot
|
|
func (m *Manager) EnableService(namespace string, serviceType ServiceType) error {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
m.logger.Info("Enabling systemd service",
|
|
zap.String("service", svcName),
|
|
zap.String("namespace", namespace))
|
|
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "enable", svcName)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to enable %s: %w; output: %s", svcName, err, string(output))
|
|
}
|
|
|
|
m.logger.Info("Service enabled successfully", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
|
|
// DisableService disables a namespace service
|
|
func (m *Manager) DisableService(namespace string, serviceType ServiceType) error {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
m.logger.Info("Disabling systemd service",
|
|
zap.String("service", svcName),
|
|
zap.String("namespace", namespace))
|
|
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "disable", svcName)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
// Don't error if service is already disabled or doesn't exist
|
|
if strings.Contains(string(output), "not loaded") {
|
|
m.logger.Debug("Service not loaded", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
return fmt.Errorf("failed to disable %s: %w; output: %s", svcName, err, string(output))
|
|
}
|
|
|
|
m.logger.Info("Service disabled successfully", zap.String("service", svcName))
|
|
return nil
|
|
}
|
|
|
|
// IsServiceActive checks if a namespace service is active
|
|
func (m *Manager) IsServiceActive(namespace string, serviceType ServiceType) (bool, error) {
|
|
svcName := m.serviceName(namespace, serviceType)
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "is-active", svcName)
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
outputStr := strings.TrimSpace(string(output))
|
|
m.logger.Debug("Checking service status",
|
|
zap.String("service", svcName),
|
|
zap.String("status", outputStr),
|
|
zap.Error(err))
|
|
|
|
if err != nil {
|
|
// is-active returns exit code 3 if service is inactive/activating
|
|
if outputStr == "inactive" || outputStr == "failed" {
|
|
m.logger.Debug("Service is not active",
|
|
zap.String("service", svcName),
|
|
zap.String("status", outputStr))
|
|
return false, nil
|
|
}
|
|
// "activating" means the service is starting - return false to wait longer, but no error
|
|
if outputStr == "activating" {
|
|
m.logger.Debug("Service is still activating",
|
|
zap.String("service", svcName))
|
|
return false, nil
|
|
}
|
|
m.logger.Error("Failed to check service status",
|
|
zap.String("service", svcName),
|
|
zap.Error(err),
|
|
zap.String("output", outputStr))
|
|
return false, fmt.Errorf("failed to check service status: %w; output: %s", err, outputStr)
|
|
}
|
|
|
|
isActive := outputStr == "active"
|
|
m.logger.Debug("Service status check complete",
|
|
zap.String("service", svcName),
|
|
zap.Bool("active", isActive))
|
|
return isActive, nil
|
|
}
|
|
|
|
// ReloadDaemon reloads systemd daemon configuration
|
|
func (m *Manager) ReloadDaemon() error {
|
|
m.logger.Info("Reloading systemd daemon")
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "daemon-reload")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
return fmt.Errorf("failed to reload systemd daemon: %w; output: %s", err, string(output))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StopAllNamespaceServices stops all namespace services for a given namespace
|
|
func (m *Manager) StopAllNamespaceServices(namespace string) error {
|
|
m.logger.Info("Stopping all namespace services", zap.String("namespace", namespace))
|
|
|
|
// Stop in reverse dependency order: Gateway → Olric → RQLite
|
|
services := []ServiceType{ServiceTypeGateway, ServiceTypeOlric, ServiceTypeRQLite}
|
|
for _, svcType := range services {
|
|
if err := m.StopService(namespace, svcType); err != nil {
|
|
m.logger.Warn("Failed to stop service",
|
|
zap.String("namespace", namespace),
|
|
zap.String("service_type", string(svcType)),
|
|
zap.Error(err))
|
|
// Continue stopping other services even if one fails
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// StartAllNamespaceServices starts all namespace services for a given namespace
|
|
func (m *Manager) StartAllNamespaceServices(namespace string) error {
|
|
m.logger.Info("Starting all namespace services", zap.String("namespace", namespace))
|
|
|
|
// Start in dependency order: RQLite → Olric → Gateway
|
|
services := []ServiceType{ServiceTypeRQLite, ServiceTypeOlric, ServiceTypeGateway}
|
|
for _, svcType := range services {
|
|
if err := m.StartService(namespace, svcType); err != nil {
|
|
return fmt.Errorf("failed to start %s service: %w", svcType, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ListNamespaceServices returns all namespace services currently registered in systemd
|
|
func (m *Manager) ListNamespaceServices() ([]string, error) {
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "list-units", "--all", "--no-legend", "debros-namespace-*@*.service")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list namespace services: %w; output: %s", err, string(output))
|
|
}
|
|
|
|
var services []string
|
|
lines := strings.Split(strings.TrimSpace(string(output)), "\n")
|
|
for _, line := range lines {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) > 0 {
|
|
services = append(services, fields[0])
|
|
}
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
// StopAllNamespaceServicesGlobally stops ALL namespace services on this node (for upgrade/maintenance)
|
|
func (m *Manager) StopAllNamespaceServicesGlobally() error {
|
|
m.logger.Info("Stopping all namespace services globally")
|
|
|
|
services, err := m.ListNamespaceServices()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list services: %w", err)
|
|
}
|
|
|
|
for _, svc := range services {
|
|
m.logger.Info("Stopping service", zap.String("service", svc))
|
|
cmd := exec.Command("sudo", "-n", "systemctl", "stop", svc)
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
m.logger.Warn("Failed to stop service",
|
|
zap.String("service", svc),
|
|
zap.Error(err),
|
|
zap.String("output", string(output)))
|
|
// Continue stopping other services
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CleanupOrphanedProcesses finds and kills any orphaned namespace processes not managed by systemd
|
|
// This is for cleaning up after migration from old exec.Command approach
|
|
func (m *Manager) CleanupOrphanedProcesses() error {
|
|
m.logger.Info("Cleaning up orphaned namespace processes")
|
|
|
|
// Find processes listening on namespace ports (10000-10999 range)
|
|
// This is a safety measure during migration
|
|
cmd := exec.Command("bash", "-c", "lsof -ti:10000-10999 2>/dev/null | xargs -r kill -TERM 2>/dev/null || true")
|
|
if output, err := cmd.CombinedOutput(); err != nil {
|
|
m.logger.Debug("Orphaned process cleanup completed",
|
|
zap.Error(err),
|
|
zap.String("output", string(output)))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateEnvFile creates the environment file for a namespace service
|
|
func (m *Manager) GenerateEnvFile(namespace, nodeID string, serviceType ServiceType, envVars map[string]string) error {
|
|
envDir := filepath.Join(m.namespaceBase, namespace)
|
|
m.logger.Debug("Creating env directory",
|
|
zap.String("dir", envDir))
|
|
|
|
if err := os.MkdirAll(envDir, 0755); err != nil {
|
|
m.logger.Error("Failed to create env directory",
|
|
zap.String("dir", envDir),
|
|
zap.Error(err))
|
|
return fmt.Errorf("failed to create env directory: %w", err)
|
|
}
|
|
|
|
envFile := filepath.Join(envDir, fmt.Sprintf("%s.env", serviceType))
|
|
|
|
var content strings.Builder
|
|
content.WriteString("# Auto-generated environment file for namespace service\n")
|
|
content.WriteString(fmt.Sprintf("# Namespace: %s\n", namespace))
|
|
content.WriteString(fmt.Sprintf("# Node ID: %s\n", nodeID))
|
|
content.WriteString(fmt.Sprintf("# Service: %s\n\n", serviceType))
|
|
|
|
// Always include NODE_ID
|
|
content.WriteString(fmt.Sprintf("NODE_ID=%s\n", nodeID))
|
|
|
|
// Add all other environment variables
|
|
for key, value := range envVars {
|
|
content.WriteString(fmt.Sprintf("%s=%s\n", key, value))
|
|
}
|
|
|
|
m.logger.Debug("Writing env file",
|
|
zap.String("file", envFile),
|
|
zap.Int("size", content.Len()))
|
|
|
|
if err := os.WriteFile(envFile, []byte(content.String()), 0644); err != nil {
|
|
m.logger.Error("Failed to write env file",
|
|
zap.String("file", envFile),
|
|
zap.Error(err))
|
|
return fmt.Errorf("failed to write env file: %w", err)
|
|
}
|
|
|
|
m.logger.Info("Generated environment file",
|
|
zap.String("file", envFile),
|
|
zap.String("namespace", namespace),
|
|
zap.String("service_type", string(serviceType)))
|
|
|
|
return nil
|
|
}
|
|
|
|
// InstallTemplateUnits installs the systemd template unit files
|
|
func (m *Manager) InstallTemplateUnits(sourceDir string) error {
|
|
m.logger.Info("Installing systemd template units", zap.String("source", sourceDir))
|
|
|
|
templates := []string{
|
|
"debros-namespace-rqlite@.service",
|
|
"debros-namespace-olric@.service",
|
|
"debros-namespace-gateway@.service",
|
|
}
|
|
|
|
for _, template := range templates {
|
|
source := filepath.Join(sourceDir, template)
|
|
dest := filepath.Join(m.systemdDir, template)
|
|
|
|
data, err := os.ReadFile(source)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read template %s: %w", template, err)
|
|
}
|
|
|
|
if err := os.WriteFile(dest, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write template %s: %w", template, err)
|
|
}
|
|
|
|
m.logger.Info("Installed template unit", zap.String("template", template))
|
|
}
|
|
|
|
// Reload systemd daemon to recognize new templates
|
|
if err := m.ReloadDaemon(); err != nil {
|
|
return fmt.Errorf("failed to reload systemd daemon: %w", err)
|
|
}
|
|
|
|
m.logger.Info("All template units installed successfully")
|
|
return nil
|
|
}
|