fix mostly bugs on system management, and user roles and group assignment
This commit is contained in:
@@ -20,16 +20,37 @@ type NTPSettings struct {
|
||||
|
||||
// Service handles system management operations
|
||||
type Service struct {
|
||||
logger *logger.Logger
|
||||
logger *logger.Logger
|
||||
rrdService *RRDService
|
||||
}
|
||||
|
||||
// NewService creates a new system service
|
||||
func NewService(log *logger.Logger) *Service {
|
||||
// Initialize RRD service for network monitoring (default to eth0, can be configured)
|
||||
rrdDir := "/var/lib/calypso/rrd"
|
||||
interfaceName := "eth0" // Default interface, can be made configurable
|
||||
rrdService := NewRRDService(log, rrdDir, interfaceName)
|
||||
|
||||
return &Service{
|
||||
logger: log,
|
||||
logger: log,
|
||||
rrdService: rrdService,
|
||||
}
|
||||
}
|
||||
|
||||
// StartNetworkMonitoring starts the RRD collector for network monitoring
|
||||
func (s *Service) StartNetworkMonitoring(ctx context.Context) error {
|
||||
return s.rrdService.StartCollector(ctx, 10*time.Second)
|
||||
}
|
||||
|
||||
// GetNetworkThroughput fetches network throughput data from RRD
|
||||
func (s *Service) GetNetworkThroughput(ctx context.Context, duration time.Duration) ([]NetworkDataPoint, error) {
|
||||
endTime := time.Now()
|
||||
startTime := endTime.Add(-duration)
|
||||
|
||||
// Use 10 second resolution for recent data
|
||||
return s.rrdService.FetchRRDData(ctx, startTime, endTime, "10")
|
||||
}
|
||||
|
||||
// ServiceStatus represents a systemd service status
|
||||
type ServiceStatus struct {
|
||||
Name string `json:"name"`
|
||||
@@ -42,31 +63,37 @@ type ServiceStatus struct {
|
||||
|
||||
// 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]),
|
||||
Name: serviceName,
|
||||
}
|
||||
|
||||
// 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
|
||||
// Get each property individually to ensure correct parsing
|
||||
properties := map[string]*string{
|
||||
"ActiveState": &status.ActiveState,
|
||||
"SubState": &status.SubState,
|
||||
"LoadState": &status.LoadState,
|
||||
"Description": &status.Description,
|
||||
}
|
||||
|
||||
for prop, target := range properties {
|
||||
cmd := exec.CommandContext(ctx, "systemctl", "show", serviceName, "--property", prop, "--value", "--no-pager")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to get property", "service", serviceName, "property", prop, "error", err)
|
||||
continue
|
||||
}
|
||||
*target = strings.TrimSpace(string(output))
|
||||
}
|
||||
|
||||
// Get timestamp if available
|
||||
cmd := exec.CommandContext(ctx, "systemctl", "show", serviceName, "--property", "ActiveEnterTimestamp", "--value", "--no-pager")
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
timestamp := strings.TrimSpace(string(output))
|
||||
if timestamp != "" {
|
||||
if t, err := time.Parse("Mon 2006-01-02 15:04:05 MST", timestamp); err == nil {
|
||||
status.Since = t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +103,15 @@ func (s *Service) GetServiceStatus(ctx context.Context, serviceName string) (*Se
|
||||
// ListServices lists all Calypso-related services
|
||||
func (s *Service) ListServices(ctx context.Context) ([]ServiceStatus, error) {
|
||||
services := []string{
|
||||
"ssh",
|
||||
"sshd",
|
||||
"smbd",
|
||||
"iscsi-scst",
|
||||
"nfs-server",
|
||||
"nfs",
|
||||
"mhvtl",
|
||||
"calypso-api",
|
||||
"scst",
|
||||
"iscsi-scst",
|
||||
"mhvtl",
|
||||
"postgresql",
|
||||
}
|
||||
|
||||
@@ -135,6 +167,108 @@ func (s *Service) GetJournalLogs(ctx context.Context, serviceName string, lines
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// SystemLogEntry represents a parsed system log entry
|
||||
type SystemLogEntry struct {
|
||||
Time string `json:"time"`
|
||||
Level string `json:"level"`
|
||||
Source string `json:"source"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GetSystemLogs retrieves recent system logs from journalctl
|
||||
func (s *Service) GetSystemLogs(ctx context.Context, limit int) ([]SystemLogEntry, error) {
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 30 // Default to 30 logs
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "journalctl",
|
||||
"-n", fmt.Sprintf("%d", limit),
|
||||
"-o", "json",
|
||||
"--no-pager",
|
||||
"--since", "1 hour ago") // Only get logs from last hour
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get system logs: %w", err)
|
||||
}
|
||||
|
||||
var logs []SystemLogEntry
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse timestamp (__REALTIME_TIMESTAMP is in microseconds)
|
||||
var timeStr string
|
||||
if timestamp, ok := logEntry["__REALTIME_TIMESTAMP"].(float64); ok {
|
||||
// Convert microseconds to nanoseconds for time.Unix (1 microsecond = 1000 nanoseconds)
|
||||
t := time.Unix(0, int64(timestamp)*1000)
|
||||
timeStr = t.Format("15:04:05")
|
||||
} else if timestamp, ok := logEntry["_SOURCE_REALTIME_TIMESTAMP"].(float64); ok {
|
||||
t := time.Unix(0, int64(timestamp)*1000)
|
||||
timeStr = t.Format("15:04:05")
|
||||
} else {
|
||||
timeStr = time.Now().Format("15:04:05")
|
||||
}
|
||||
|
||||
// Parse log level (priority)
|
||||
level := "INFO"
|
||||
if priority, ok := logEntry["PRIORITY"].(float64); ok {
|
||||
switch int(priority) {
|
||||
case 0: // emerg
|
||||
level = "EMERG"
|
||||
case 1, 2, 3: // alert, crit, err
|
||||
level = "ERROR"
|
||||
case 4: // warning
|
||||
level = "WARN"
|
||||
case 5: // notice
|
||||
level = "NOTICE"
|
||||
case 6: // info
|
||||
level = "INFO"
|
||||
case 7: // debug
|
||||
level = "DEBUG"
|
||||
}
|
||||
}
|
||||
|
||||
// Parse source (systemd unit or syslog identifier)
|
||||
source := "system"
|
||||
if unit, ok := logEntry["_SYSTEMD_UNIT"].(string); ok && unit != "" {
|
||||
// Remove .service suffix if present
|
||||
source = strings.TrimSuffix(unit, ".service")
|
||||
} else if ident, ok := logEntry["SYSLOG_IDENTIFIER"].(string); ok && ident != "" {
|
||||
source = ident
|
||||
} else if comm, ok := logEntry["_COMM"].(string); ok && comm != "" {
|
||||
source = comm
|
||||
}
|
||||
|
||||
// Parse message
|
||||
message := ""
|
||||
if msg, ok := logEntry["MESSAGE"].(string); ok {
|
||||
message = msg
|
||||
}
|
||||
|
||||
if message != "" {
|
||||
logs = append(logs, SystemLogEntry{
|
||||
Time: timeStr,
|
||||
Level: level,
|
||||
Source: source,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse to get newest first
|
||||
for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 {
|
||||
logs[i], logs[j] = logs[j], logs[i]
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
// GenerateSupportBundle generates a diagnostic support bundle
|
||||
func (s *Service) GenerateSupportBundle(ctx context.Context, outputPath string) error {
|
||||
// Create bundle directory
|
||||
@@ -314,33 +448,57 @@ func (s *Service) ListNetworkInterfaces(ctx context.Context) ([]NetworkInterface
|
||||
lines = strings.Split(string(output), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse default route: "default via 10.10.14.1 dev ens18"
|
||||
if strings.HasPrefix(line, "default via ") {
|
||||
// Format: "default via 192.168.1.1 dev ens18"
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 4 && parts[2] == "dev" {
|
||||
gateway := parts[1]
|
||||
ifaceName := parts[3]
|
||||
// Find "via" and "dev" in the parts
|
||||
var gateway string
|
||||
var ifaceName string
|
||||
for i, part := range parts {
|
||||
if part == "via" && i+1 < len(parts) {
|
||||
gateway = parts[i+1]
|
||||
}
|
||||
if part == "dev" && i+1 < len(parts) {
|
||||
ifaceName = parts[i+1]
|
||||
}
|
||||
}
|
||||
if gateway != "" && ifaceName != "" {
|
||||
if iface, exists := interfaceMap[ifaceName]; exists {
|
||||
iface.Gateway = gateway
|
||||
s.logger.Debug("Set gateway for interface", "name", ifaceName, "gateway", gateway)
|
||||
s.logger.Info("Set default gateway for interface", "name", ifaceName, "gateway", gateway)
|
||||
}
|
||||
}
|
||||
} else if strings.Contains(line, " via ") && strings.Contains(line, " dev ") {
|
||||
// Format: "10.10.14.0/24 via 10.10.14.1 dev ens18"
|
||||
// Parse network route: "10.10.14.0/24 via 10.10.14.1 dev ens18"
|
||||
// Or: "192.168.1.0/24 via 192.168.1.1 dev eth0"
|
||||
parts := strings.Fields(line)
|
||||
var gateway string
|
||||
var ifaceName string
|
||||
for i, part := range parts {
|
||||
if part == "via" && i+1 < len(parts) && i+2 < len(parts) && parts[i+2] == "dev" {
|
||||
gateway := parts[i+1]
|
||||
ifaceName := parts[i+3]
|
||||
if iface, exists := interfaceMap[ifaceName]; exists {
|
||||
if part == "via" && i+1 < len(parts) {
|
||||
gateway = parts[i+1]
|
||||
}
|
||||
if part == "dev" && i+1 < len(parts) {
|
||||
ifaceName = parts[i+1]
|
||||
}
|
||||
}
|
||||
// Only set gateway if it's not already set (prefer default route)
|
||||
if gateway != "" && ifaceName != "" {
|
||||
if iface, exists := interfaceMap[ifaceName]; exists {
|
||||
if iface.Gateway == "" {
|
||||
iface.Gateway = gateway
|
||||
s.logger.Debug("Set gateway for interface", "name", ifaceName, "gateway", gateway)
|
||||
s.logger.Info("Set gateway from network route for interface", "name", ifaceName, "gateway", gateway)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
s.logger.Warn("Failed to get routes", "error", err)
|
||||
}
|
||||
|
||||
// Get DNS servers from systemd-resolved or /etc/resolv.conf
|
||||
@@ -437,6 +595,123 @@ func (s *Service) ListNetworkInterfaces(ctx context.Context) ([]NetworkInterface
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
// UpdateNetworkInterfaceRequest represents the request to update a network interface
|
||||
type UpdateNetworkInterfaceRequest struct {
|
||||
IPAddress string `json:"ip_address"`
|
||||
Subnet string `json:"subnet"`
|
||||
Gateway string `json:"gateway,omitempty"`
|
||||
DNS1 string `json:"dns1,omitempty"`
|
||||
DNS2 string `json:"dns2,omitempty"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateNetworkInterface updates network interface configuration
|
||||
func (s *Service) UpdateNetworkInterface(ctx context.Context, ifaceName string, req UpdateNetworkInterfaceRequest) (*NetworkInterface, error) {
|
||||
// Validate interface exists
|
||||
cmd := exec.CommandContext(ctx, "ip", "link", "show", ifaceName)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("interface %s not found: %w", ifaceName, err)
|
||||
}
|
||||
|
||||
// Remove existing IP address if any
|
||||
cmd = exec.CommandContext(ctx, "ip", "addr", "flush", "dev", ifaceName)
|
||||
cmd.Run() // Ignore error, interface might not have IP
|
||||
|
||||
// Set new IP address and subnet
|
||||
ipWithSubnet := fmt.Sprintf("%s/%s", req.IPAddress, req.Subnet)
|
||||
cmd = exec.CommandContext(ctx, "ip", "addr", "add", ipWithSubnet, "dev", ifaceName)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to set IP address", "interface", ifaceName, "error", err, "output", string(output))
|
||||
return nil, fmt.Errorf("failed to set IP address: %w", err)
|
||||
}
|
||||
|
||||
// Remove existing default route if any
|
||||
cmd = exec.CommandContext(ctx, "ip", "route", "del", "default")
|
||||
cmd.Run() // Ignore error, might not exist
|
||||
|
||||
// Set gateway if provided
|
||||
if req.Gateway != "" {
|
||||
cmd = exec.CommandContext(ctx, "ip", "route", "add", "default", "via", req.Gateway, "dev", ifaceName)
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to set gateway", "interface", ifaceName, "error", err, "output", string(output))
|
||||
return nil, fmt.Errorf("failed to set gateway: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update DNS in systemd-resolved or /etc/resolv.conf
|
||||
if req.DNS1 != "" || req.DNS2 != "" {
|
||||
// Try using systemd-resolve first
|
||||
cmd = exec.CommandContext(ctx, "systemd-resolve", "--status")
|
||||
if cmd.Run() == nil {
|
||||
// systemd-resolve is available, use it
|
||||
dnsServers := []string{}
|
||||
if req.DNS1 != "" {
|
||||
dnsServers = append(dnsServers, req.DNS1)
|
||||
}
|
||||
if req.DNS2 != "" {
|
||||
dnsServers = append(dnsServers, req.DNS2)
|
||||
}
|
||||
if len(dnsServers) > 0 {
|
||||
// Use resolvectl to set DNS (newer systemd)
|
||||
cmd = exec.CommandContext(ctx, "resolvectl", "dns", ifaceName, strings.Join(dnsServers, " "))
|
||||
if cmd.Run() != nil {
|
||||
// Fallback to systemd-resolve
|
||||
cmd = exec.CommandContext(ctx, "systemd-resolve", "--interface", ifaceName, "--set-dns", strings.Join(dnsServers, " "))
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to set DNS via systemd-resolve", "error", err, "output", string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback: update /etc/resolv.conf
|
||||
resolvContent := "# Generated by Calypso\n"
|
||||
if req.DNS1 != "" {
|
||||
resolvContent += fmt.Sprintf("nameserver %s\n", req.DNS1)
|
||||
}
|
||||
if req.DNS2 != "" {
|
||||
resolvContent += fmt.Sprintf("nameserver %s\n", req.DNS2)
|
||||
}
|
||||
|
||||
tmpPath := "/tmp/resolv.conf." + fmt.Sprintf("%d", time.Now().Unix())
|
||||
if err := os.WriteFile(tmpPath, []byte(resolvContent), 0644); err != nil {
|
||||
s.logger.Warn("Failed to write temporary resolv.conf", "error", err)
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("mv %s /etc/resolv.conf", tmpPath))
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to update /etc/resolv.conf", "error", err, "output", string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bring interface up
|
||||
cmd = exec.CommandContext(ctx, "ip", "link", "set", ifaceName, "up")
|
||||
output, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
s.logger.Warn("Failed to bring interface up", "interface", ifaceName, "error", err, "output", string(output))
|
||||
}
|
||||
|
||||
// Return updated interface
|
||||
updatedIface := &NetworkInterface{
|
||||
Name: ifaceName,
|
||||
IPAddress: req.IPAddress,
|
||||
Subnet: req.Subnet,
|
||||
Gateway: req.Gateway,
|
||||
DNS1: req.DNS1,
|
||||
DNS2: req.DNS2,
|
||||
Role: req.Role,
|
||||
Status: "Connected",
|
||||
Speed: "Unknown", // Will be updated on next list
|
||||
}
|
||||
|
||||
s.logger.Info("Updated network interface", "interface", ifaceName, "ip", req.IPAddress, "subnet", req.Subnet)
|
||||
return updatedIface, nil
|
||||
}
|
||||
|
||||
// SaveNTPSettings saves NTP configuration to the OS
|
||||
func (s *Service) SaveNTPSettings(ctx context.Context, settings NTPSettings) error {
|
||||
// Set timezone using timedatectl
|
||||
|
||||
Reference in New Issue
Block a user