diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..79d6ed3 --- /dev/null +++ b/.air.toml @@ -0,0 +1,44 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = ["--debug", "serve"] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "json"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = true + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/go.mod b/go.mod index 42e6f50..62f58b3 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect diff --git a/go.sum b/go.sum index 39e5dcc..4aff90f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= diff --git a/index.html b/index.html index de2a497..2cdd2f4 100644 --- a/index.html +++ b/index.html @@ -1,207 +1,567 @@ - - - Server Management - - - + + + + + + + Nexus Node | Server Management + + + + + + + + + + +
+ + + + + + +
+ + -
- -
- -
+ + + + + + + +
+ +
+ + + + + + - - - \ No newline at end of file + + diff --git a/main.go b/main.go index 6570e47..4e47b30 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "os" @@ -11,48 +12,58 @@ import ( ) func main() { + debug := flag.Bool("debug", false, "Enable debug logging") + flag.Parse() + + if *debug { + serveractions.Debug = true + serveractions.LogDebug("Debug mode enabled") + } + + args := flag.Args() + if len(args) < 1 { + printUsage() + return + } + servers, err := loadConfig() if err != nil { fmt.Printf("Error loading servers: %v\n", err) return } + serveractions.LogDebug("Loaded %d servers from config", len(servers)) - if len(os.Args) < 2 { - printUsage() - return - } - - command := os.Args[1] + command := args[0] switch command { case "list": listServers(servers) case "wake": - if len(os.Args) < 3 { + if len(args) < 2 { fmt.Println("Please specify a server name to wake.") printUsage() return } - serverName := os.Args[2] + serverName := args[1] serveractions.WakeServer(serverName, servers) case "wakeall": serveractions.WakeAllServers(servers) case "status": serveractions.CheckServersStatus(servers) case "shutdown": - if len(os.Args) < 3 { + if len(args) < 2 { fmt.Println("Please specify a server name to shutdown.") printUsage() return } - serverName := os.Args[2] + serverName := args[1] serveractions.ShutdownServer(serverName, servers) case "reboot": - if len(os.Args) < 3 { + if len(args) < 2 { fmt.Println("Please specify a server name to reboot.") printUsage() return } - serverName := os.Args[2] + serverName := args[1] serveractions.RebootServer(serverName, servers) case "serve": webserver.StartWebServer(servers) @@ -63,7 +74,9 @@ func main() { } func printUsage() { - fmt.Println("Usage: go run . ") + fmt.Println("Usage: go run . [--debug] ") + fmt.Println("Options:") + fmt.Println(" --debug - Enable debug logging") fmt.Println("Commands:") fmt.Println(" list - List all configured servers") fmt.Println(" wake - Wake a specific server") @@ -75,14 +88,15 @@ func printUsage() { } func loadConfig() ([]serveractions.Server, error) { - viper.SetConfigName("servers") // name of config file (without extension) - viper.SetConfigType("json") // or viper.SetConfigType("YAML") - viper.AddConfigPath("./config") // path to look for the config file in - viper.AddConfigPath(".") // optionally look for config in the working directory - err := viper.ReadInConfig() // Find and read the config file - if err != nil { // Handle errors reading the config file + viper.SetConfigName("servers") // name of config file (without extension) + viper.SetConfigType("json") // or viper.SetConfigType("YAML") + viper.AddConfigPath("./config") // path to look for the config file in + viper.AddConfigPath(".") // optionally look for config in the working directory + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file // Check if the error is that the file doesn't exist if _, ok := err.(viper.ConfigFileNotFoundError); ok { + os.WriteFile("debug_config.log", []byte("Config file not found\n"), 0644) // Config file not found; ignore error and return empty server list return []serveractions.Server{}, nil } diff --git a/server-actions/server_actions.go b/server-actions/server_actions.go index 221efd4..6a5d5d7 100644 --- a/server-actions/server_actions.go +++ b/server-actions/server_actions.go @@ -4,38 +4,78 @@ import ( "context" "fmt" "net" + "os" "os/exec" + "regexp" + "strings" "time" "golang.org/x/crypto/ssh" ) +var Debug bool + +func LogDebug(format string, v ...interface{}) { + if Debug { + fmt.Fprintf(os.Stderr, "[DEBUG] "+format+"\n", v...) + } +} + 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) + err := sendMagicPacket(s.Mac, s.IP) if err != nil { - fmt.Printf("Error sending packet: %v\n", err) + fmt.Printf("Error sending packet from local network: %v\n", err) } else { - fmt.Println("Packet sent successfully.") + fmt.Println("Local WOL packet sent successfully.") } + + // Try to wake via proxy if there's another online server in the same subnet + LogDebug("Checking for online servers in the same subnet to use as a proxy...") + for _, other := range servers { + if other.Name != s.Name && other.IP != "" && strings.HasPrefix(other.IP, s.IP[:strings.LastIndex(s.IP, ".")]) { + // Only use as proxy if we have credentials + if other.SSHUser != "" && other.SSHPass != "" { + if PingHost(other.IP) { + fmt.Printf("Attempting to wake %s via proxy %s...\n", s.Name, other.Name) + err := WakeServerRemote(other, s.Mac) + if err != nil { + LogDebug("Failed to wake via proxy %s: %v", other.Name, err) + } else { + fmt.Printf("Wake signal sent via proxy %s.\n", other.Name) + return + } + } + } + } + } + return } } fmt.Printf("Server '%s' not found in configuration.\n", name) } +func WakeServerRemote(proxy Server, targetMac string) error { + // Send magic packet from the proxy server using a simple shell command + // We use python or similar if available, or just a simple bash command that constructs the packet + // constructing the packet in bash: + // echo -e $(printf 'f%.0s' {1..12}; printf "$(echo $MAC | sed 's/://g')%.0s" {1..16}) | xxd -r -p | nc -w1 -u -b 255.255.255.255 9 + + // A simpler way if wakeonlan is installed: wakeonlan $MAC + // But let's assume we might need to construct it or use a common tool. + // We'll try common tools first. + cmd := fmt.Sprintf("wakeonlan %s || ether-wake %s || (printf '%%b' \"$(printf '\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff\\\\xff'; for i in {1..16}; do printf '%s' | sed 's/\\([0-9a-fA-F]\\{2\\}\\)/\\\\x\\1/g'; done)\" | nc -w1 -u -b 255.255.255.255 9)", targetMac, targetMac, targetMac) + + return executeSSHCommand(proxy, cmd) +} + 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) - } + WakeServer(s.Name, servers) } } @@ -56,17 +96,24 @@ func CheckServersStatus(servers []Server) { } func PingHost(host string) bool { + LogDebug("Pinging host: %s", host) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "1", host) err := cmd.Run() + if err != nil { + LogDebug("Ping failed for %s: %v", host, err) + } else { + LogDebug("Ping successful for %s", host) + } return err == nil } // Creates and sends a magic packet to the specified MAC address. -func sendMagicPacket(macAddr string) error { +// Supports cross-VLAN by sending to both general and subnet-directed broadcast. +func sendMagicPacket(macAddr string, ip string) error { hwAddr, err := net.ParseMAC(macAddr) if err != nil { return fmt.Errorf("invalid mac address '%s': %w", macAddr, err) @@ -80,14 +127,39 @@ func sendMagicPacket(macAddr string) error { copy(magicPacket[i*6:], hwAddr) } - conn, err := net.Dial("udp", "255.255.255.255:9") - if err != nil { - return err - } - defer conn.Close() + // List of addresses to try sending the WOL packet to + // List of addresses to try sending the WOL packet to + broadcastAddresses := []string{"255.255.255.255"} - _, err = conn.Write(magicPacket) - return err + // If a valid IP is provided, calculate the directed broadcast address (assuming /24 subnet) + if ip != "" { + ipParts := strings.Split(ip, ".") + if len(ipParts) == 4 { + directedBroadcast := fmt.Sprintf("%s.%s.%s.255", ipParts[0], ipParts[1], ipParts[2]) + if directedBroadcast != "255.255.255.255" { + broadcastAddresses = append(broadcastAddresses, directedBroadcast) + } + broadcastAddresses = append(broadcastAddresses, ip) + } + } + + ports := []int{7, 9} + for _, addr := range broadcastAddresses { + for _, port := range ports { + fullAddr := fmt.Sprintf("%s:%d", addr, port) + LogDebug("Sending magic packet to %s", fullAddr) + for i := 0; i < 3; i++ { + conn, err := net.Dial("udp", fullAddr) + if err == nil { + conn.Write(magicPacket) + conn.Close() + } + time.Sleep(10 * time.Millisecond) + } + } + } + + return nil } func ShutdownServer(name string, servers []Server) { @@ -122,32 +194,149 @@ func RebootServer(name string, servers []Server) { fmt.Printf("Server '%s' not found in configuration.\n", name) } -func executeSSHCommand(server Server, command string) error { +func GetSSHClient(server Server) (*ssh.Client, error) { config := &ssh.ClientConfig{ User: server.SSHUser, Auth: []ssh.AuthMethod{ + // Explicitly using ONLY Password and Keyboard-Interactive to avoid + // trying local SSH keys which might trigger "Too many attempts" ssh.Password(server.SSHPass), + ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range questions { + answers[i] = server.SSHPass + } + return answers, nil + }), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 10 * time.Second, } - client, err := ssh.Dial("tcp", server.IP+":22", config) + return ssh.Dial("tcp", server.IP+":22", config) +} + +func executeSSHCommand(server Server, command string) error { + client, err := GetSSHClient(server) if err != nil { - return fmt.Errorf("failed to dial: %w", err) + return fmt.Errorf("failed to connect: %w", err) } defer client.Close() + LogDebug("SSH connection established to %s", server.IP) + session, err := client.NewSession() if err != nil { return fmt.Errorf("failed to create session: %w", err) } defer session.Close() - _, err = session.CombinedOutput(command) + LogDebug("Executing SSH command: %s", command) + output, err := session.CombinedOutput(command) if err != nil { + LogDebug("SSH command failed: %v, Output: %s", err, string(output)) return fmt.Errorf("failed to run command: %w", err) } + LogDebug("SSH command output: %s", string(output)) return nil } +func GetMacAddress(ip string) (string, error) { + // Ping the host to ensure it's in the ARP table + PingHost(ip) + + LogDebug("Looking for MAC address for IP: %s", ip) + // Run arp -a to get the ARP table + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "arp", "-a") + output, err := cmd.CombinedOutput() + if err != nil { + LogDebug("arp -a failed: %v", err) + return "", fmt.Errorf("error running arp: %v", err) + } + + LogDebug("arp output received (%d bytes)", len(output)) + // Regex to find the MAC address for the given IP + // Matches: (IP) at MAC or variations + lines := strings.Split(string(output), "\n") + for _, line := range lines { + if strings.Contains(line, "("+ip+")") || strings.Contains(line, " "+ip+" ") { + LogDebug("Found matching line in arp table: %s", line) + // Extract MAC address - usually looking for 6 pairs of hex digits separated by : or - + re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`) + mac := re.FindString(line) + if mac != "" { + LogDebug("Extracted MAC: %s", mac) + return mac, nil + } + } + } + + LogDebug("No MAC address found in arp table for IP %s", ip) + return "", fmt.Errorf("MAC address not found for IP %s", ip) +} + +func GetMacAddressSSH(ip, user, pass string) (string, error) { + if user == "" || pass == "" { + return "", fmt.Errorf("SSH credentials required for remote MAC lookup") + } + + LogDebug("Attempting to get MAC address via SSH for %s@%s", user, ip) + + server := Server{ + IP: ip, + SSHUser: user, + SSHPass: pass, + } + + // Try common commands to get MAC on Linux/Unix + // ip link, ifconfig, etc. + cmd := "cat /sys/class/net/$(ip route get 8.8.8.8 | awk '{print $5}')/address 2>/dev/null || ifconfig | grep -E 'ether|HWaddr' | awk '{print $2}' | head -n 1" + + config := &ssh.ClientConfig{ + User: server.SSHUser, + Auth: []ssh.AuthMethod{ + ssh.Password(server.SSHPass), + ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) { + answers := make([]string, len(questions)) + for i := range questions { + answers[i] = server.SSHPass + } + return answers, nil + }), + }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 5 * time.Second, + } + + client, err := ssh.Dial("tcp", server.IP+":22", config) + if err != nil { + return "", fmt.Errorf("SSH dial failed: %w", err) + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + return "", fmt.Errorf("failed to create session: %w", err) + } + defer session.Close() + + output, err := session.CombinedOutput(cmd) + if err != nil { + return "", fmt.Errorf("SSH command failed: %w", err) + } + + mac := strings.TrimSpace(string(output)) + // Validate MAC format + re := regexp.MustCompile(`([0-9a-fA-F]{1,2}[:\-]){5}[0-9a-fA-F]{1,2}`) + mac = re.FindString(mac) + + if mac == "" { + return "", fmt.Errorf("could not extract MAC address from SSH output") + } + + LogDebug("Successfully retrieved MAC via SSH: %s", mac) + return mac, nil +} diff --git a/server-actions/types.go b/server-actions/types.go index 3fdac67..eb98ffb 100644 --- a/server-actions/types.go +++ b/server-actions/types.go @@ -2,9 +2,9 @@ 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"` + Name string `json:"name" mapstructure:"name"` + Mac string `json:"mac" mapstructure:"mac"` + IP string `json:"ip" mapstructure:"ip"` + SSHUser string `json:"ssh_user" mapstructure:"ssh_user"` + SSHPass string `json:"ssh_pass" mapstructure:"ssh_pass"` } diff --git a/webserver/terminal.go b/webserver/terminal.go new file mode 100644 index 0000000..8a77be0 --- /dev/null +++ b/webserver/terminal.go @@ -0,0 +1,153 @@ +package webserver + +import ( + "fmt" + "log" + "net/http" + + serveractions "manage-servers/server-actions" + + "github.com/gorilla/websocket" + "golang.org/x/crypto/ssh" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return true // Allowing all origins for local tool + }, +} + +func (ws *WebServer) handleSSH(w http.ResponseWriter, r *http.Request) { + serverName := r.URL.Query().Get("name") + if serverName == "" { + http.Error(w, "Missing server name", http.StatusBadRequest) + return + } + + var targetServer *serveractions.Server + for _, s := range ws.servers { + if s.Name == serverName { + targetServer = &s + break + } + } + + if targetServer == nil { + http.Error(w, "Server not found", http.StatusNotFound) + return + } + + if targetServer.SSHUser == "" || targetServer.SSHPass == "" { + conn, err := upgrader.Upgrade(w, r, nil) + if err == nil { + conn.WriteMessage(websocket.TextMessage, []byte("\r\n[Error] SSH Credentials missing for this node.\r\nPlease edit the server entry and save with a Username and Password.\r\n")) + conn.Close() + } + return + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("Websocket upgrade failed: %v", err) + return + } + defer conn.Close() + + client, err := serveractions.GetSSHClient(*targetServer) + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] SSH Connection failed: %v\r\n", err))) + return + } + defer client.Close() + + session, err := client.NewSession() + if err != nil { + conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] SSH Session failed: %v\r\n", err))) + return + } + defer session.Close() + + // Request pseudo-terminal + modes := ssh.TerminalModes{ + ssh.ECHO: 1, + ssh.TTY_OP_ISPEED: 14400, + ssh.TTY_OP_OSPEED: 14400, + } + + // Default to 24x80 if not provided + rows := 24 + cols := 80 + if r.URL.Query().Get("rows") != "" { + fmt.Sscanf(r.URL.Query().Get("rows"), "%d", &rows) + } + if r.URL.Query().Get("cols") != "" { + fmt.Sscanf(r.URL.Query().Get("cols"), "%d", &cols) + } + + if err := session.RequestPty("xterm-256color", rows, cols, modes); err != nil { + conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] Request for PTY failed: %v\r\n", err))) + return + } + + stdin, err := session.StdinPipe() + if err != nil { + return + } + stdout, err := session.StdoutPipe() + if err != nil { + return + } + stderr, err := session.StderrPipe() + if err != nil { + return + } + + if err := session.Shell(); err != nil { + conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\r\n[Error] Shell start failed: %v\r\n", err))) + return + } + + // Proxy stdout/stderr to WebSocket + go func() { + buf := make([]byte, 1024) + for { + n, err := stdout.Read(buf) + if n > 0 { + if err := conn.WriteMessage(websocket.TextMessage, buf[:n]); err != nil { + return + } + } + if err != nil { + return + } + } + }() + + go func() { + buf := make([]byte, 1024) + for { + n, err := stderr.Read(buf) + if n > 0 { + if err := conn.WriteMessage(websocket.TextMessage, buf[:n]); err != nil { + return + } + } + if err != nil { + return + } + } + }() + + // Proxy WebSocket to stdin + for { + _, message, err := conn.ReadMessage() + if err != nil { + break + } + if _, err := stdin.Write(message); err != nil { + break + } + } +} diff --git a/webserver/webserver.go b/webserver/webserver.go index b9ea9ce..5d40cd8 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -24,6 +24,8 @@ func StartWebServer(servers []serveractions.Server) { http.HandleFunc("/shutdown", ws.handleShutdownServer) http.HandleFunc("/reboot", ws.handleRebootServer) http.HandleFunc("/status", ws.handleStatus) + http.HandleFunc("/find-mac", ws.handleFindMac) + http.HandleFunc("/ssh", ws.handleSSH) if err := http.ListenAndServe(":8080", nil); err != nil { fmt.Printf("Error starting server: %v\n", err) @@ -31,15 +33,19 @@ func StartWebServer(servers []serveractions.Server) { } func (ws *WebServer) handleRoot(w http.ResponseWriter, r *http.Request) { + serveractions.LogDebug("Handling root request: %s %s", r.Method, r.URL.Path) http.ServeFile(w, r, "index.html") } func (ws *WebServer) handleServers(w http.ResponseWriter, r *http.Request) { + serveractions.LogDebug("Handling /servers request: %s", r.Method) switch r.Method { case http.MethodGet: ws.getServers(w, r) case http.MethodPost: ws.addServer(w, r) + case http.MethodPut: + ws.updateServer(w, r) case http.MethodDelete: ws.deleteServer(w, r) default: @@ -52,6 +58,42 @@ func (ws *WebServer) getServers(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(ws.servers) } +func (ws *WebServer) updateServer(w http.ResponseWriter, r *http.Request) { + oldName := r.URL.Query().Get("oldName") + if oldName == "" { + http.Error(w, "Missing old server name", http.StatusBadRequest) + return + } + + var updatedServer serveractions.Server + if err := json.NewDecoder(r.Body).Decode(&updatedServer); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + found := false + for i, server := range ws.servers { + if server.Name == oldName { + ws.servers[i] = updatedServer + found = true + break + } + } + + if !found { + http.Error(w, "Server not found", http.StatusNotFound) + return + } + + viper.Set("servers", ws.servers) + if err := viper.WriteConfig(); err != nil { + http.Error(w, fmt.Sprintf("Error writing config file: %v", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + func (ws *WebServer) addServer(w http.ResponseWriter, r *http.Request) { var newServer serveractions.Server if err := json.NewDecoder(r.Body).Decode(&newServer); err != nil { @@ -143,6 +185,7 @@ func (ws *WebServer) handleRebootServer(w http.ResponseWriter, r *http.Request) func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) { serverName := r.URL.Query().Get("name") + serveractions.LogDebug("Checking status for server: %s", serverName) if serverName == "" { http.Error(w, "Missing server name", http.StatusBadRequest) return @@ -162,3 +205,36 @@ func (ws *WebServer) handleStatus(w http.ResponseWriter, r *http.Request) { http.Error(w, "Server not found", http.StatusNotFound) } + +func (ws *WebServer) handleFindMac(w http.ResponseWriter, r *http.Request) { + ip := r.URL.Query().Get("ip") + user := r.URL.Query().Get("user") + pass := r.URL.Query().Get("pass") + + serveractions.LogDebug("Handling /find-mac request for IP: %s (SSH provided: %v)", ip, user != "") + if ip == "" { + http.Error(w, "Missing IP address", http.StatusBadRequest) + return + } + + // Try ARP first + mac, err := serveractions.GetMacAddress(ip) + if err != nil { + serveractions.LogDebug("ARP lookup failed for %s: %v. Trying SSH fallback...", ip, err) + // Try SSH fallback if credentials are provided + if user != "" && pass != "" { + mac, err = serveractions.GetMacAddressSSH(ip, user, pass) + if err != nil { + serveractions.LogDebug("SSH lookup also failed for %s: %v", ip, err) + http.Error(w, "MAC address not found via ARP or SSH", http.StatusNotFound) + return + } + } else { + serveractions.LogDebug("No SSH credentials for fallback for %s", ip) + http.Error(w, "MAC address not found in local network. Please provide SSH credentials for remote lookup.", http.StatusNotFound) + return + } + } + + w.Write([]byte(mac)) +}