package main import ( "context" "encoding/json" "fmt" "log" "os" "os/signal" "runtime" "strings" "syscall" "time" "nexusrmm.local/agent/internal/collector" "nexusrmm.local/agent/internal/config" "nexusrmm.local/agent/internal/connection" "nexusrmm.local/agent/internal/deployer" "nexusrmm.local/agent/internal/executor" "nexusrmm.local/agent/internal/meshagent" "nexusrmm.local/agent/internal/scanner" "nexusrmm.local/agent/internal/updater" pb "nexusrmm.local/agent/pkg/proto" ) var version = "dev" func main() { log.Printf("NexusRMM Agent %s starting on %s/%s", version, runtime.GOOS, runtime.GOARCH) cfg, err := config.Load("nexus-agent.yaml") if err != nil { log.Fatalf("Config load error: %v", err) } client, err := connection.ConnectWithRetry(cfg.ServerAddress, 10) if err != nil { log.Fatalf("Failed to connect: %v", err) } defer client.Close() if cfg.AgentID == "" { hostname, _ := os.Hostname() metrics, _ := collector.Collect() // Beste Netzwerkschnittstelle für Enrollment wählen (bevorzuge IPv4) mac, ip := "", "" for _, n := range metrics.Networks { if n.MAC == "" || n.IPAddress == "" { continue } if mac == "" { mac, ip = n.MAC, n.IPAddress } // Sobald IPv4 gefunden: nehmen und fertig if !strings.Contains(n.IPAddress, ":") { mac, ip = n.MAC, n.IPAddress break } } resp, err := client.Client.Enroll(context.Background(), &pb.EnrollRequest{ Hostname: hostname, OsType: runtime.GOOS, OsVersion: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), MacAddress: mac, IpAddress: ip, AgentVersion: version, }) if err != nil { log.Fatalf("Enrollment failed: %v", err) } cfg.AgentID = resp.AgentId cfg.HeartbeatInterval = int(resp.HeartbeatInterval) if err := cfg.Save("nexus-agent.yaml"); err != nil { log.Printf("Warning: could not save config: %v", err) } log.Printf("Enrolled with ID: %s", cfg.AgentID) } // MeshAgent installieren falls konfiguriert if cfg.MeshEnabled && cfg.MeshCentralUrl != "" { log.Printf("Installiere MeshAgent von %s...", cfg.MeshCentralUrl) if err := meshagent.Install(context.Background(), cfg.MeshCentralUrl); err != nil { log.Printf("MeshAgent-Installation fehlgeschlagen (nicht kritisch): %v", err) } } ctx, cancel := context.WithCancel(context.Background()) defer cancel() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) ticker := time.NewTicker(time.Duration(cfg.HeartbeatInterval) * time.Second) defer ticker.Stop() log.Printf("Agent running. Heartbeat every %ds", cfg.HeartbeatInterval) doHeartbeat(ctx, client, cfg) for { select { case <-ticker.C: doHeartbeat(ctx, client, cfg) case <-sigCh: log.Println("Shutting down...") return case <-ctx.Done(): return } } } func doHeartbeat(ctx context.Context, client *connection.GrpcClient, cfg *config.Config) { metrics, err := collector.Collect() if err != nil { log.Printf("Metric collection error: %v", err) return } req := &pb.HeartbeatRequest{ AgentId: cfg.AgentID, Metrics: &pb.SystemMetrics{ CpuUsagePercent: metrics.CPUPercent, MemoryUsagePercent: metrics.MemoryPercent, MemoryTotalBytes: int64(metrics.MemoryTotal), MemoryAvailableBytes: int64(metrics.MemoryAvailable), UptimeSeconds: metrics.UptimeSeconds, }, } for _, d := range metrics.Disks { req.Metrics.Disks = append(req.Metrics.Disks, &pb.DiskInfo{ MountPoint: d.MountPoint, TotalBytes: int64(d.Total), FreeBytes: int64(d.Free), Filesystem: d.Filesystem, }) } resp, err := client.Client.Heartbeat(ctx, req) if err != nil { log.Printf("Heartbeat error: %v", err) return } for _, cmd := range resp.PendingCommands { log.Printf("Executing command %s (type: %v)", cmd.CommandId, cmd.Type) go executeCommand(ctx, client, cfg, cmd) } } func executeCommand(ctx context.Context, client *connection.GrpcClient, cfg *config.Config, cmd *pb.AgentCommand) { agentID := cfg.AgentID var result *executor.Result switch cmd.Type { case pb.CommandType_COMMAND_TYPE_SHELL: result = executor.Execute(ctx, cmd.Payload, 300) case pb.CommandType_COMMAND_TYPE_INSTALL_SOFTWARE: result = deployer.Install(ctx, cmd.Payload) case pb.CommandType_COMMAND_TYPE_UNINSTALL_SOFTWARE: result = deployer.Uninstall(ctx, cmd.Payload) case pb.CommandType_COMMAND_TYPE_NETWORK_SCAN: var params struct { Subnet string `json:"subnet"` } _ = json.Unmarshal([]byte(cmd.Payload), ¶ms) devices, err := scanner.Scan(ctx, params.Subnet) if err != nil { result = &executor.Result{ExitCode: 1, Stderr: err.Error()} } else { result = &executor.Result{ ExitCode: 0, Stdout: scanner.ToJSON(devices), Success: true, } } case pb.CommandType_COMMAND_TYPE_UPDATE_AGENT: // Payload optional: {"serverAddress": "localhost:5000"} var params struct { ServerAddress string `json:"serverAddress"` } _ = json.Unmarshal([]byte(cmd.Payload), ¶ms) if params.ServerAddress == "" { // Aus gRPC-Adresse REST-Adresse ableiten (Port 5000 statt 5001) params.ServerAddress = strings.Replace(cfg.ServerAddress, "5001", "5000", 1) } info, err := updater.CheckForUpdate(ctx, params.ServerAddress, version) if err != nil { result = &executor.Result{ExitCode: 1, Stderr: err.Error()} } else if info == nil { result = &executor.Result{ExitCode: 0, Stdout: "Bereits aktuell: " + version, Success: true} } else { if updateErr := updater.Update(ctx, info); updateErr != nil { result = &executor.Result{ExitCode: 1, Stderr: updateErr.Error()} } else { result = &executor.Result{ExitCode: 0, Stdout: "Update auf " + info.Version + " gestartet", Success: true} } } default: result = &executor.Result{ExitCode: -1, Stderr: fmt.Sprintf("unknown command type: %v", cmd.Type)} } if err := func() error { _, err := client.Client.ReportCommandResult(ctx, &pb.CommandResult{ AgentId: agentID, CommandId: cmd.CommandId, ExitCode: int32(result.ExitCode), Stdout: result.Stdout, Stderr: result.Stderr, Success: result.Success, }) return err }(); err != nil { log.Printf("Failed to report result for %s: %v", cmd.CommandId, err) } }