348 lines
11 KiB
Go
348 lines
11 KiB
Go
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
|
|
}
|
|
|
|
// NetworkInterface represents a network interface
|
|
type NetworkInterface struct {
|
|
Name string `json:"name"`
|
|
IPAddress string `json:"ip_address"`
|
|
Subnet string `json:"subnet"`
|
|
Status string `json:"status"` // "Connected" or "Down"
|
|
Speed string `json:"speed"` // e.g., "10 Gbps", "1 Gbps"
|
|
Role string `json:"role"` // "Management", "ISCSI", or empty
|
|
}
|
|
|
|
// ListNetworkInterfaces lists all network interfaces
|
|
func (s *Service) ListNetworkInterfaces(ctx context.Context) ([]NetworkInterface, error) {
|
|
// First, get all interface names and their states
|
|
cmd := exec.CommandContext(ctx, "ip", "link", "show")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
s.logger.Error("Failed to list interfaces", "error", err)
|
|
return nil, fmt.Errorf("failed to list interfaces: %w", err)
|
|
}
|
|
|
|
interfaceMap := make(map[string]*NetworkInterface)
|
|
lines := strings.Split(string(output), "\n")
|
|
|
|
s.logger.Debug("Parsing network interfaces", "output_lines", len(lines))
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse interface name and state
|
|
// Format: "2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000"
|
|
// Look for lines that start with a number followed by ":" (interface definition line)
|
|
// Simple check: line starts with digit, contains ":", and contains "state"
|
|
if len(line) > 0 && line[0] >= '0' && line[0] <= '9' && strings.Contains(line, ":") && strings.Contains(line, "state") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) < 2 {
|
|
continue
|
|
}
|
|
|
|
// Extract interface name (e.g., "ens18:" or "lo:")
|
|
ifaceName := strings.TrimSuffix(parts[1], ":")
|
|
if ifaceName == "" || ifaceName == "lo" {
|
|
continue // Skip loopback
|
|
}
|
|
|
|
// Extract state - look for "state UP" or "state DOWN" in the line
|
|
state := "Down"
|
|
if strings.Contains(line, "state UP") {
|
|
state = "Connected"
|
|
} else if strings.Contains(line, "state DOWN") {
|
|
state = "Down"
|
|
}
|
|
|
|
s.logger.Info("Found interface", "name", ifaceName, "state", state)
|
|
|
|
interfaceMap[ifaceName] = &NetworkInterface{
|
|
Name: ifaceName,
|
|
Status: state,
|
|
Speed: "Unknown",
|
|
}
|
|
}
|
|
}
|
|
|
|
s.logger.Debug("Found interfaces from ip link", "count", len(interfaceMap))
|
|
|
|
// Get IP addresses for each interface
|
|
cmd = exec.CommandContext(ctx, "ip", "-4", "addr", "show")
|
|
output, err = cmd.Output()
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get IP addresses", "error", err)
|
|
} else {
|
|
lines = strings.Split(string(output), "\n")
|
|
var currentIfaceName string
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
// Parse interface name (e.g., "2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP>")
|
|
if strings.Contains(line, ":") && !strings.Contains(line, "inet") && !strings.HasPrefix(line, "valid_lft") && !strings.HasPrefix(line, "altname") {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
currentIfaceName = strings.TrimSuffix(parts[1], ":")
|
|
s.logger.Debug("Processing interface for IP", "name", currentIfaceName)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Parse IP address (e.g., "inet 10.10.14.16/24 brd 10.10.14.255 scope global ens18")
|
|
if strings.HasPrefix(line, "inet ") && currentIfaceName != "" && currentIfaceName != "lo" {
|
|
parts := strings.Fields(line)
|
|
if len(parts) >= 2 {
|
|
ipWithSubnet := parts[1] // e.g., "10.10.14.16/24"
|
|
ipParts := strings.Split(ipWithSubnet, "/")
|
|
if len(ipParts) == 2 {
|
|
ip := ipParts[0]
|
|
subnet := ipParts[1]
|
|
|
|
// Find or create interface
|
|
iface, exists := interfaceMap[currentIfaceName]
|
|
if !exists {
|
|
s.logger.Debug("Creating new interface entry", "name", currentIfaceName)
|
|
iface = &NetworkInterface{
|
|
Name: currentIfaceName,
|
|
Status: "Down",
|
|
Speed: "Unknown",
|
|
}
|
|
interfaceMap[currentIfaceName] = iface
|
|
}
|
|
|
|
iface.IPAddress = ip
|
|
iface.Subnet = subnet
|
|
s.logger.Debug("Set IP for interface", "name", currentIfaceName, "ip", ip, "subnet", subnet)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert map to slice
|
|
var interfaces []NetworkInterface
|
|
s.logger.Debug("Converting interface map to slice", "map_size", len(interfaceMap))
|
|
for _, iface := range interfaceMap {
|
|
// Get speed for each interface using ethtool
|
|
if iface.Name != "" && iface.Name != "lo" {
|
|
cmd := exec.CommandContext(ctx, "ethtool", iface.Name)
|
|
output, err := cmd.Output()
|
|
if err == nil {
|
|
// Parse speed from ethtool output
|
|
ethtoolLines := strings.Split(string(output), "\n")
|
|
for _, ethtoolLine := range ethtoolLines {
|
|
if strings.Contains(ethtoolLine, "Speed:") {
|
|
parts := strings.Fields(ethtoolLine)
|
|
if len(parts) >= 2 {
|
|
iface.Speed = parts[1]
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Determine role based on interface name or IP (simple heuristic)
|
|
// You can enhance this with configuration file or database lookup
|
|
if strings.Contains(iface.Name, "eth") || strings.Contains(iface.Name, "ens") {
|
|
// Default to Management for first interface, ISCSI for others
|
|
if iface.Name == "eth0" || iface.Name == "ens18" {
|
|
iface.Role = "Management"
|
|
} else {
|
|
// Check if IP is in typical iSCSI range (10.x.x.x)
|
|
if strings.HasPrefix(iface.IPAddress, "10.") && iface.IPAddress != "" {
|
|
iface.Role = "ISCSI"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
interfaces = append(interfaces, *iface)
|
|
}
|
|
|
|
// If no interfaces found, return empty slice
|
|
if len(interfaces) == 0 {
|
|
s.logger.Warn("No network interfaces found")
|
|
return []NetworkInterface{}, nil
|
|
}
|
|
|
|
s.logger.Info("Listed network interfaces", "count", len(interfaces))
|
|
return interfaces, nil
|
|
}
|