343 lines
9.9 KiB
Go
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
|
|
}
|