package system import ( "context" "encoding/json" "fmt" "os/exec" "strings" "time" "github.com/atlasos/calypso/internal/common/logger" ) // Service handles system management operations type Service struct { logger *logger.Logger } // NewService creates a new system service func NewService(log *logger.Logger) *Service { return &Service{ logger: log, } } // ServiceStatus represents a systemd service status type ServiceStatus struct { Name string `json:"name"` ActiveState string `json:"active_state"` SubState string `json:"sub_state"` LoadState string `json:"load_state"` Description string `json:"description"` Since time.Time `json:"since,omitempty"` } // GetServiceStatus retrieves the status of a systemd service func (s *Service) GetServiceStatus(ctx context.Context, serviceName string) (*ServiceStatus, error) { cmd := exec.CommandContext(ctx, "systemctl", "show", serviceName, "--property=ActiveState,SubState,LoadState,Description,ActiveEnterTimestamp", "--value", "--no-pager") output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get service status: %w", err) } lines := strings.Split(strings.TrimSpace(string(output)), "\n") if len(lines) < 4 { return nil, fmt.Errorf("invalid service status output") } status := &ServiceStatus{ Name: serviceName, ActiveState: strings.TrimSpace(lines[0]), SubState: strings.TrimSpace(lines[1]), LoadState: strings.TrimSpace(lines[2]), Description: strings.TrimSpace(lines[3]), } // Parse timestamp if available if len(lines) > 4 && lines[4] != "" { if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", strings.TrimSpace(lines[4])); err == nil { status.Since = t } } return status, nil } // ListServices lists all Calypso-related services func (s *Service) ListServices(ctx context.Context) ([]ServiceStatus, error) { services := []string{ "calypso-api", "scst", "iscsi-scst", "mhvtl", "postgresql", } var statuses []ServiceStatus for _, serviceName := range services { status, err := s.GetServiceStatus(ctx, serviceName) if err != nil { s.logger.Warn("Failed to get service status", "service", serviceName, "error", err) continue } statuses = append(statuses, *status) } return statuses, nil } // RestartService restarts a systemd service func (s *Service) RestartService(ctx context.Context, serviceName string) error { cmd := exec.CommandContext(ctx, "systemctl", "restart", serviceName) output, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("failed to restart service: %s: %w", string(output), err) } s.logger.Info("Service restarted", "service", serviceName) return nil } // GetJournalLogs retrieves journald logs for a service func (s *Service) GetJournalLogs(ctx context.Context, serviceName string, lines int) ([]map[string]interface{}, error) { cmd := exec.CommandContext(ctx, "journalctl", "-u", serviceName, "-n", fmt.Sprintf("%d", lines), "-o", "json", "--no-pager") output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("failed to get logs: %w", err) } var logs []map[string]interface{} linesOutput := strings.Split(strings.TrimSpace(string(output)), "\n") for _, line := range linesOutput { if line == "" { continue } var logEntry map[string]interface{} if err := json.Unmarshal([]byte(line), &logEntry); err == nil { logs = append(logs, logEntry) } } return logs, nil } // GenerateSupportBundle generates a diagnostic support bundle func (s *Service) GenerateSupportBundle(ctx context.Context, outputPath string) error { // Create bundle directory cmd := exec.CommandContext(ctx, "mkdir", "-p", outputPath) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to create bundle directory: %w", err) } // Collect system information commands := map[string][]string{ "system_info": {"uname", "-a"}, "disk_usage": {"df", "-h"}, "memory": {"free", "-h"}, "scst_status": {"scstadmin", "-list_target"}, "services": {"systemctl", "list-units", "--type=service", "--state=running"}, } for name, cmdArgs := range commands { cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) output, err := cmd.CombinedOutput() if err != nil { s.logger.Warn("Failed to collect info", "command", name, "error", err) continue } // Write to file filePath := fmt.Sprintf("%s/%s.txt", outputPath, name) if err := exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("cat > %s", filePath)).Run(); err == nil { exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("echo '%s' > %s", string(output), filePath)).Run() } } // Collect journal logs services := []string{"calypso-api", "scst", "iscsi-scst"} for _, service := range services { cmd := exec.CommandContext(ctx, "journalctl", "-u", service, "-n", "1000", "--no-pager") output, err := cmd.CombinedOutput() if err == nil { filePath := fmt.Sprintf("%s/journal_%s.log", outputPath, service) exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("echo '%s' > %s", string(output), filePath)).Run() } } s.logger.Info("Support bundle generated", "path", outputPath) return nil }