feat: füge Unterstützung für das Hinzufügen und Löschen von Servern hinzu, aktualisiere die Konfiguration und verbessere die Benutzeroberfläche
Build And Test / build (push) Successful in 54s
Build And Test / Build and Publish Docker Image (push) Failing after 21s

This commit is contained in:
Matthias Hinrichs
2025-11-22 02:24:02 +01:00
parent 303f0ec9d3
commit be381205be
6 changed files with 265 additions and 30 deletions
+2
View File
@@ -13,6 +13,8 @@ services:
- "traefik.http.routers.manage-servers.tls=true" - "traefik.http.routers.manage-servers.tls=true"
- "traefik.http.routers.manage-servers.tls.certresolver=cloudflare" - "traefik.http.routers.manage-servers.tls.certresolver=cloudflare"
- "traefik.http.services.manage-servers.loadbalancer.server.port=8080" - "traefik.http.services.manage-servers.loadbalancer.server.port=8080"
volumes:
- /volume1/docker-data/manage-servers:/config
networks: networks:
- my-container-macvlan-200 - my-container-macvlan-200
+15 -1
View File
@@ -4,4 +4,18 @@ go 1.24.6
require golang.org/x/crypto v0.45.0 require golang.org/x/crypto v0.45.0
require golang.org/x/sys v0.38.0 // indirect require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // 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
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
)
+25
View File
@@ -1,6 +1,31 @@
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
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/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=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+112 -1
View File
@@ -8,7 +8,7 @@
<div class="container mx-auto p-8"> <div class="container mx-auto p-8">
<h1 class="text-4xl font-bold mb-8">Server Management</h1> <h1 class="text-4xl font-bold mb-8">Server Management</h1>
<div class="bg-white shadow-md rounded-lg overflow-hidden"> <div id="server-table-container" class="bg-white shadow-md rounded-lg overflow-hidden">
<table class="min-w-full leading-normal"> <table class="min-w-full leading-normal">
<thead> <thead>
<tr> <tr>
@@ -24,19 +24,110 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="no-servers-message" class="hidden bg-white shadow-md rounded-lg p-8 text-center">
<p class="text-gray-600">No servers found. Please check your configuration.</p>
</div>
<div class="mt-8">
<button id="add-server-button" class="bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
Add New Server
</button>
</div>
<div id="add-server-form-container" class="hidden mt-8 bg-white shadow-md rounded-lg p-8">
<h2 class="text-2xl font-bold mb-4">Add New Server</h2>
<form id="add-server-form">
<div class="mb-4">
<label for="name" class="block text-gray-700 text-sm font-bold mb-2">Name</label>
<input type="text" id="name" name="name" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
</div>
<div class="mb-4">
<label for="ip" class="block text-gray-700 text-sm font-bold mb-2">IP Address</label>
<input type="text" id="ip" name="ip" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
</div>
<div class="mb-4">
<label for="mac" class="block text-gray-700 text-sm font-bold mb-2">MAC Address</label>
<input type="text" id="mac" name="mac" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" required>
</div>
<div class="mb-4">
<label for="ssh_user" class="block text-gray-700 text-sm font-bold mb-2">SSH User</label>
<input type="text" id="ssh_user" name="ssh_user" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="mb-4">
<label for="ssh_pass" class="block text-gray-700 text-sm font-bold mb-2">SSH Password</label>
<input type="password" id="ssh_pass" name="ssh_pass" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline">
</div>
<div class="flex items-center justify-between">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Save Server
</button>
<button type="button" id="cancel-add-server" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">
Cancel
</button>
</div>
</form>
</div>
</div> </div>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
fetchServers(); fetchServers();
const addServerButton = document.getElementById("add-server-button");
const addServerFormContainer = document.getElementById("add-server-form-container");
const cancelAddServerButton = document.getElementById("cancel-add-server");
const addServerForm = document.getElementById("add-server-form");
addServerButton.addEventListener("click", () => {
addServerFormContainer.classList.remove("hidden");
addServerButton.classList.add("hidden");
});
cancelAddServerButton.addEventListener("click", () => {
addServerFormContainer.classList.add("hidden");
addServerButton.classList.remove("hidden");
addServerForm.reset();
});
addServerForm.addEventListener("submit", function(event) {
event.preventDefault();
const formData = new FormData(addServerForm);
const serverData = Object.fromEntries(formData.entries());
fetch("/servers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(serverData),
})
.then(response => {
if (response.ok) {
fetchServers();
addServerFormContainer.classList.add("hidden");
addServerButton.classList.remove("hidden");
addServerForm.reset();
} else {
alert("Failed to add server.");
}
});
});
}); });
function fetchServers() { function fetchServers() {
fetch("/servers") fetch("/servers")
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const tableContainer = document.getElementById("server-table-container");
const noServersMessage = document.getElementById("no-servers-message");
const tableBody = document.getElementById("server-table-body"); const tableBody = document.getElementById("server-table-body");
tableBody.innerHTML = ""; // Clear existing rows tableBody.innerHTML = ""; // Clear existing rows
if (data && data.length > 0) {
tableContainer.classList.remove("hidden");
noServersMessage.classList.add("hidden");
data.forEach(server => { data.forEach(server => {
const row = document.createElement("tr"); const row = document.createElement("tr");
row.innerHTML = ` row.innerHTML = `
@@ -48,11 +139,16 @@
<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="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="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> <button onclick="rebootServer('${server.name}')" class="bg-yellow-500 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded">Reboot</button>
<button onclick="deleteServer('${server.name}')" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">Delete</button>
</td> </td>
`; `;
tableBody.appendChild(row); tableBody.appendChild(row);
checkServerStatus(server.name); checkServerStatus(server.name);
}); });
} else {
tableContainer.classList.add("hidden");
noServersMessage.classList.remove("hidden");
}
}); });
} }
@@ -88,6 +184,21 @@
} }
} }
function deleteServer(serverName) {
if (confirm(`Are you sure you want to delete ${serverName}? This action cannot be undone.`)) {
fetch(`/servers?name=${serverName}`, {
method: 'DELETE',
})
.then(response => {
if (response.ok) {
fetchServers(); // Refresh the server list
} else {
alert(`Failed to delete ${serverName}.`);
}
});
}
}
// Refresh server status every 30 seconds // Refresh server status every 30 seconds
setInterval(fetchServers, 30000); setInterval(fetchServers, 30000);
</script> </script>
+22 -13
View File
@@ -1,16 +1,17 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
serveractions "manage-servers/server-actions" serveractions "manage-servers/server-actions"
"manage-servers/webserver" "manage-servers/webserver"
"github.com/spf13/viper"
) )
func main() { func main() {
servers, err := loadServers("./config/servers.json") servers, err := loadConfig()
if err != nil { if err != nil {
fmt.Printf("Error loading servers: %v\n", err) fmt.Printf("Error loading servers: %v\n", err)
return return
@@ -73,18 +74,26 @@ func printUsage() {
fmt.Println(" serve - Start a web server to manage servers") fmt.Println(" serve - Start a web server to manage servers")
} }
func loadServers(filename string) ([]serveractions.Server, error) { 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
// Check if the error is that the file doesn't exist
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error and return empty server list
return []serveractions.Server{}, nil
}
return nil, fmt.Errorf("fatal error config file: %w", err)
}
var servers []serveractions.Server var servers []serveractions.Server
err = viper.UnmarshalKey("servers", &servers)
file, err := os.ReadFile(filename)
if err != nil { if err != nil {
fmt.Println("No servers configuration found, please create a servers.json file in the config folder.") return nil, fmt.Errorf("unable to decode into struct, %v", err)
} else { }
err = json.Unmarshal(file, &servers)
if err != nil {
return nil, err
}}
return servers, nil return servers, nil
} }
@@ -92,6 +101,6 @@ func loadServers(filename string) ([]serveractions.Server, error) {
func listServers(servers []serveractions.Server) { func listServers(servers []serveractions.Server) {
fmt.Println("Configured Servers:") fmt.Println("Configured Servers:")
for _, s := range servers { for _, s := range servers {
fmt.Printf(" - %s (%s) - %s\n", s.Name, s.IP, s.Mac) fmt.Printf(" - %s (%s) - %s - User: %s\n", s.Name, s.IP, s.Mac, s.SSHUser)
} }
} }
+77 -3
View File
@@ -5,7 +5,9 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"manage-servers/server-actions" serveractions "manage-servers/server-actions"
"github.com/spf13/viper"
) )
type WebServer struct { type WebServer struct {
@@ -17,7 +19,7 @@ func StartWebServer(servers []serveractions.Server) {
fmt.Println("Starting web server on :8080...") fmt.Println("Starting web server on :8080...")
http.HandleFunc("/", ws.handleRoot) http.HandleFunc("/", ws.handleRoot)
http.HandleFunc("/servers", ws.handleGetServers) http.HandleFunc("/servers", ws.handleServers)
http.HandleFunc("/wake", ws.handleWakeServer) http.HandleFunc("/wake", ws.handleWakeServer)
http.HandleFunc("/shutdown", ws.handleShutdownServer) http.HandleFunc("/shutdown", ws.handleShutdownServer)
http.HandleFunc("/reboot", ws.handleRebootServer) http.HandleFunc("/reboot", ws.handleRebootServer)
@@ -32,11 +34,83 @@ func (ws *WebServer) handleRoot(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "index.html") http.ServeFile(w, r, "index.html")
} }
func (ws *WebServer) handleGetServers(w http.ResponseWriter, r *http.Request) { func (ws *WebServer) handleServers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
ws.getServers(w, r)
case http.MethodPost:
ws.addServer(w, r)
case http.MethodDelete:
ws.deleteServer(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (ws *WebServer) getServers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(ws.servers) json.NewEncoder(w).Encode(ws.servers)
} }
func (ws *WebServer) addServer(w http.ResponseWriter, r *http.Request) {
var newServer serveractions.Server
if err := json.NewDecoder(r.Body).Decode(&newServer); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
ws.servers = append(ws.servers, newServer)
viper.Set("servers", ws.servers)
if err := viper.WriteConfig(); err != nil {
// If the file does not exist, create it
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
if err = viper.SafeWriteConfig(); err != nil {
http.Error(w, fmt.Sprintf("Error creating config file: %v", err), http.StatusInternalServerError)
return
}
} else {
http.Error(w, fmt.Sprintf("Error writing config file: %v", err), http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusCreated)
}
func (ws *WebServer) deleteServer(w http.ResponseWriter, r *http.Request) {
serverName := r.URL.Query().Get("name")
if serverName == "" {
http.Error(w, "Missing server name", http.StatusBadRequest)
return
}
var found bool
var updatedServers []serveractions.Server
for _, server := range ws.servers {
if server.Name != serverName {
updatedServers = append(updatedServers, server)
} else {
found = true
}
}
if !found {
http.Error(w, "Server not found", http.StatusNotFound)
return
}
ws.servers = updatedServers
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) handleWakeServer(w http.ResponseWriter, r *http.Request) { func (ws *WebServer) handleWakeServer(w http.ResponseWriter, r *http.Request) {
serverName := r.URL.Query().Get("name") serverName := r.URL.Query().Get("name")
if serverName == "" { if serverName == "" {