feat: Initial commit of the server management tool

This commit is contained in:
Matthias
2025-09-08 20:36:50 +02:00
commit 75406bd56c
8 changed files with 474 additions and 0 deletions
+15
View File
@@ -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
+7
View File
@@ -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
+6
View File
@@ -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=
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html>
<head>
<title>Server Management</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 text-gray-800">
<div class="container mx-auto p-8">
<h1 class="text-4xl font-bold mb-8">Server Management</h1>
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<table class="min-w-full leading-normal">
<thead>
<tr>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Name</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">IP</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">MAC</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Status</th>
<th class="px-5 py-3 border-b-2 border-gray-200 bg-gray-100 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody id="server-table-body">
<!-- Server rows will be inserted here -->
</tbody>
</table>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function() {
fetchServers();
});
function fetchServers() {
fetch("/servers")
.then(response => response.json())
.then(data => {
const tableBody = document.getElementById("server-table-body");
tableBody.innerHTML = ""; // Clear existing rows
data.forEach(server => {
const row = document.createElement("tr");
row.innerHTML = `
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.name}</td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.ip}</td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">${server.mac}</td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm" id="status-${server.name}">Checking...</td>
<td class="px-5 py-5 border-b border-gray-200 bg-white text-sm">
<button onclick="wakeServer('${server.name}')" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Wake</button>
<button onclick="shutdownServer('${server.name}')" class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded">Shutdown</button>
<button onclick="rebootServer('${server.name}')" class="bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded">Reboot</button>
</td>
`;
tableBody.appendChild(row);
checkServerStatus(server.name);
});
});
}
function checkServerStatus(serverName) {
fetch(`/status?name=${serverName}`)
.then(response => response.text())
.then(status => {
const statusCell = document.getElementById(`status-${serverName}`);
statusCell.textContent = status;
if (status === "Online") {
statusCell.classList.add("text-green-500", "font-bold");
statusCell.classList.remove("text-red-500");
} else {
statusCell.classList.add("text-red-500", "font-bold");
statusCell.classList.remove("text-green-500");
}
});
}
function wakeServer(serverName) {
fetch(`/wake?name=${serverName}`).then(() => alert(`${serverName} wake signal sent.`));
}
function shutdownServer(serverName) {
if (confirm(`Are you sure you want to shutdown ${serverName}?`)) {
fetch(`/shutdown?name=${serverName}`).then(() => alert(`${serverName} is shutting down.`));
}
}
function rebootServer(serverName) {
if (confirm(`Are you sure you want to reboot ${serverName}?`)) {
fetch(`/reboot?name=${serverName}`).then(() => alert(`${serverName} is rebooting.`));
}
}
// Refresh server status every 30 seconds
setInterval(fetchServers, 30000);
</script>
</body>
</html>
+96
View File
@@ -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 . <command>")
fmt.Println("Commands:")
fmt.Println(" list - List all configured servers")
fmt.Println(" wake <server_name> - Wake a specific server")
fmt.Println(" wakeall - Wake all configured servers")
fmt.Println(" status - Check the status of all servers")
fmt.Println(" shutdown <server_name> - Shutdown a specific server")
fmt.Println(" reboot <server_name> - 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)
}
}
+153
View File
@@ -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
}
+10
View File
@@ -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"`
}
+90
View File
@@ -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)
}