feat: Initial commit of the server management tool
This commit is contained in:
+15
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user