Files
manage-servers/server-actions/server_actions.go
T
Matthias Hinrichs 1e992a53b0
Build Docker Container using Multistage Build / build (push) Failing after 23s
refactor: Increase ping host timeout to 2 seconds and remove the -W 1 argument.
2026-02-18 01:23:39 +01:00

343 lines
9.9 KiB
Go

package serveractions
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, s.IP)
if err != nil {
fmt.Printf("Error sending packet from local network: %v\n", err)
} else {
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 {
WakeServer(s.Name, servers)
}
}
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 {
LogDebug("Pinging host: %s", host)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "ping", "-c", "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.
// 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)
}
magicPacket := make([]byte, 102)
for i := 0; i < 6; i++ {
magicPacket[i] = 0xFF
}
for i := 1; i <= 16; i++ {
copy(magicPacket[i*6:], hwAddr)
}
// 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"}
// 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) {
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 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,
}
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 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()
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
}