commit 75406bd56c7c37456e70fb2082aa2a4b594645d5 Author: Matthias Date: Mon Sep 8 20:36:50 2025 +0200 feat: Initial commit of the server management tool diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fae74ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries +manage-servers + +# Log files +*.log + +# IDE-specific files +.idea/ +.vscode/ + +# OS-specific files +.DS_Store + +# Config files containing credentials +servers.json \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ef56ad8 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module manage-servers + +go 1.24.6 + +require golang.org/x/crypto v0.42.0 + +require golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..017168b --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= diff --git a/index.html b/index.html new file mode 100644 index 0000000..ed1ff69 --- /dev/null +++ b/index.html @@ -0,0 +1,97 @@ + + + + Server Management + + + + +
+

Server Management

+ +
+ + + + + + + + + + + + + +
NameIPMACStatusActions
+
+
+ + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..1a194d0 --- /dev/null +++ b/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "manage-servers/server-actions" + "manage-servers/webserver" +) + +func main() { + servers, err := loadServers("servers.json") + if err != nil { + fmt.Printf("Error loading servers: %v\n", err) + return + } + + if len(os.Args) < 2 { + printUsage() + return + } + + command := os.Args[1] + switch command { + case "list": + listServers(servers) + case "wake": + if len(os.Args) < 3 { + fmt.Println("Please specify a server name to wake.") + printUsage() + return + } + serverName := os.Args[2] + serveractions.WakeServer(serverName, servers) + case "wakeall": + serveractions.WakeAllServers(servers) + case "status": + serveractions.CheckServersStatus(servers) + case "shutdown": + if len(os.Args) < 3 { + fmt.Println("Please specify a server name to shutdown.") + printUsage() + return + } + serverName := os.Args[2] + serveractions.ShutdownServer(serverName, servers) + case "reboot": + if len(os.Args) < 3 { + fmt.Println("Please specify a server name to reboot.") + printUsage() + return + } + serverName := os.Args[2] + serveractions.RebootServer(serverName, servers) + case "serve": + webserver.StartWebServer(servers) + default: + fmt.Printf("Unknown command: %s\n", command) + printUsage() + } +} + +func printUsage() { + fmt.Println("Usage: go run . ") + fmt.Println("Commands:") + fmt.Println(" list - List all configured servers") + fmt.Println(" wake - Wake a specific server") + fmt.Println(" wakeall - Wake all configured servers") + fmt.Println(" status - Check the status of all servers") + fmt.Println(" shutdown - Shutdown a specific server") + fmt.Println(" reboot - Reboot a specific server") + fmt.Println(" serve - Start a web server to manage servers") +} + +func loadServers(filename string) ([]serveractions.Server, error) { + file, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var servers []serveractions.Server + err = json.Unmarshal(file, &servers) + if err != nil { + return nil, err + } + + return servers, nil +} + +func listServers(servers []serveractions.Server) { + fmt.Println("Configured Servers:") + for _, s := range servers { + fmt.Printf(" - %s (%s) - %s\n", s.Name, s.IP, s.Mac) + } +} diff --git a/server-actions/server_actions.go b/server-actions/server_actions.go new file mode 100644 index 0000000..221efd4 --- /dev/null +++ b/server-actions/server_actions.go @@ -0,0 +1,153 @@ +package serveractions + +import ( + "context" + "fmt" + "net" + "os/exec" + "time" + + "golang.org/x/crypto/ssh" +) + +func WakeServer(name string, servers []Server) { + for _, s := range servers { + if s.Name == name { + fmt.Printf("Sending Wake-on-LAN packet to %s (%s)...\n", s.Name, s.Mac) + err := sendMagicPacket(s.Mac) + if err != nil { + fmt.Printf("Error sending packet: %v\n", err) + } else { + fmt.Println("Packet sent successfully.") + } + return + } + } + fmt.Printf("Server '%s' not found in configuration.\n", name) +} + +func WakeAllServers(servers []Server) { + fmt.Println("Waking all servers...") + for _, s := range servers { + fmt.Printf("Sending Wake-on-LAN packet to %s (%s)...\n", s.Name, s.Mac) + err := sendMagicPacket(s.Mac) + if err != nil { + fmt.Printf(" - Error for %s: %v\n", s.Name, err) + } else { + fmt.Printf(" - Packet sent to %s.\n", s.Name) + } + } +} + +func CheckServersStatus(servers []Server) { + fmt.Println("Checking server status...") + for _, s := range servers { + if s.IP == "" { + fmt.Printf(" - %-20s [SKIPPED] (No IP configured)\n", s.Name) + continue + } + online := PingHost(s.IP) + status := "Offline" + if online { + status = "Online" + } + fmt.Printf(" - %-20s [%s]\n", s.Name, status) + } +} + +func PingHost(host string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", host) + + err := cmd.Run() + return err == nil +} + +// Creates and sends a magic packet to the specified MAC address. +func sendMagicPacket(macAddr string) error { + hwAddr, err := net.ParseMAC(macAddr) + if err != nil { + return fmt.Errorf("invalid mac address '%s': %w", macAddr, err) + } + + magicPacket := make([]byte, 102) + for i := 0; i < 6; i++ { + magicPacket[i] = 0xFF + } + for i := 1; i <= 16; i++ { + copy(magicPacket[i*6:], hwAddr) + } + + conn, err := net.Dial("udp", "255.255.255.255:9") + if err != nil { + return err + } + defer conn.Close() + + _, err = conn.Write(magicPacket) + return err +} + +func ShutdownServer(name string, servers []Server) { + for _, s := range servers { + if s.Name == name { + fmt.Printf("Attempting to shutdown %s...\n", s.Name) + err := executeSSHCommand(s, "sudo shutdown -h now") + if err != nil { + fmt.Printf("Error shutting down %s: %v\n", s.Name, err) + } else { + fmt.Printf("%s is shutting down.\n", s.Name) + } + return + } + } + fmt.Printf("Server '%s' not found in configuration.\n", name) +} + +func RebootServer(name string, servers []Server) { + for _, s := range servers { + if s.Name == name { + fmt.Printf("Attempting to reboot %s...\n", s.Name) + err := executeSSHCommand(s, "sudo reboot") + if err != nil { + fmt.Printf("Error rebooting %s: %v\n", s.Name, err) + } else { + fmt.Printf("%s is rebooting.\n", s.Name) + } + return + } + } + fmt.Printf("Server '%s' not found in configuration.\n", name) +} + +func executeSSHCommand(server Server, command string) error { + config := &ssh.ClientConfig{ + User: server.SSHUser, + Auth: []ssh.AuthMethod{ + ssh.Password(server.SSHPass), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 10 * time.Second, + } + + client, err := ssh.Dial("tcp", server.IP+":22", config) + if err != nil { + return fmt.Errorf("failed to dial: %w", err) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("failed to create session: %w", err) + } + defer session.Close() + + _, err = session.CombinedOutput(command) + if err != nil { + return fmt.Errorf("failed to run command: %w", err) + } + + return nil +} diff --git a/server-actions/types.go b/server-actions/types.go new file mode 100644 index 0000000..3fdac67 --- /dev/null +++ b/server-actions/types.go @@ -0,0 +1,10 @@ +package serveractions + +// Server struct to hold server information +type Server struct { + Name string `json:"name"` + Mac string `json:"mac"` + IP string `json:"ip"` + SSHUser string `json:"ssh_user"` + SSHPass string `json:"ssh_pass"` +} diff --git a/webserver/webserver.go b/webserver/webserver.go new file mode 100644 index 0000000..b4b5576 --- /dev/null +++ b/webserver/webserver.go @@ -0,0 +1,90 @@ +package webserver + +import ( + "encoding/json" + "fmt" + "net/http" + + "manage-servers/server-actions" +) + +type WebServer struct { + servers []serveractions.Server +} + +func StartWebServer(servers []serveractions.Server) { + ws := &WebServer{servers: servers} + + fmt.Println("Starting web server on :8080...") + http.HandleFunc("/", ws.handleRoot) + http.HandleFunc("/servers", ws.handleGetServers) + http.HandleFunc("/wake", ws.handleWakeServer) + http.HandleFunc("/shutdown", ws.handleShutdownServer) + http.HandleFunc("/reboot", ws.handleRebootServer) + http.HandleFunc("/status", ws.handleStatus) + + if err := http.ListenAndServe(":8080", nil); err != nil { + fmt.Printf("Error starting server: %v\n", err) + } +} + +func (ws *WebServer) handleRoot(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "index.html") +} + +func (ws *WebServer) handleGetServers(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(ws.servers) +} + +func (ws *WebServer) handleWakeServer(w http.ResponseWriter, r *http.Request) { + serverName := r.URL.Query().Get("name") + if serverName == "" { + http.Error(w, "Missing server name", http.StatusBadRequest) + return + } + serveractions.WakeServer(serverName, ws.servers) + w.WriteHeader(http.StatusOK) +} + +func (ws *WebServer) handleShutdownServer(w http.ResponseWriter, r *http.Request) { + serverName := r.URL.Query().Get("name") + if serverName == "" { + http.Error(w, "Missing server name", http.StatusBadRequest) + return + } + serveractions.ShutdownServer(serverName, ws.servers) + w.WriteHeader(http.StatusOK) +} + +func (ws *WebServer) handleRebootServer(w http.ResponseWriter, r *http.Request) { + serverName := r.URL.Query().Get("name") + if serverName == "" { + http.Error(w, "Missing server name", http.StatusBadRequest) + return + } + serveractions.RebootServer(serverName, ws.servers) + w.WriteHeader(http.StatusOK) +} + +func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) { + serverName := r.URL.Query().Get("name") + if serverName == "" { + http.Error(w, "Missing server name", http.StatusBadRequest) + return + } + + for _, s := range ws.servers { + if s.Name == serverName { + online := serveractions.PingHost(s.IP) + status := "Offline" + if online { + status = "Online" + } + w.Write([]byte(status)) + return + } + } + + http.Error(w, "Server not found", http.StatusNotFound) +}