diff --git a/docker-compose.yml b/docker-compose.yml
index cf82310..a884f1d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -13,6 +13,8 @@ services:
- "traefik.http.routers.manage-servers.tls=true"
- "traefik.http.routers.manage-servers.tls.certresolver=cloudflare"
- "traefik.http.services.manage-servers.loadbalancer.server.port=8080"
+ volumes:
+ - /volume1/docker-data/manage-servers:/config
networks:
- my-container-macvlan-200
diff --git a/go.mod b/go.mod
index 5c2f9d8..c2781cd 100644
--- a/go.mod
+++ b/go.mod
@@ -4,4 +4,18 @@ go 1.24.6
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
+)
diff --git a/go.sum b/go.sum
index 0f82070..9093fbd 100644
--- a/go.sum
+++ b/go.sum
@@ -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/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
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/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/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=
diff --git a/index.html b/index.html
index ed771af..de2a497 100644
--- a/index.html
+++ b/index.html
@@ -8,7 +8,7 @@
Server Management
-
+
+
+
No servers found. Please check your configuration.
+
+
+
+
+
+
+
diff --git a/main.go b/main.go
index 1fefdde..6570e47 100644
--- a/main.go
+++ b/main.go
@@ -1,16 +1,17 @@
package main
import (
- "encoding/json"
"fmt"
"os"
serveractions "manage-servers/server-actions"
"manage-servers/webserver"
+
+ "github.com/spf13/viper"
)
func main() {
- servers, err := loadServers("./config/servers.json")
+ servers, err := loadConfig()
if err != nil {
fmt.Printf("Error loading servers: %v\n", err)
return
@@ -73,18 +74,26 @@ func printUsage() {
fmt.Println(" serve - Start a web server to manage servers")
}
-func loadServers(filename string) ([]serveractions.Server, error) {
- var servers []serveractions.Server
-
- file, err := os.ReadFile(filename)
- if err != nil {
- fmt.Println("No servers configuration found, please create a servers.json file in the config folder.")
- } else {
+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)
+ }
- err = json.Unmarshal(file, &servers)
+ var servers []serveractions.Server
+ err = viper.UnmarshalKey("servers", &servers)
if err != nil {
- return nil, err
- }}
+ return nil, fmt.Errorf("unable to decode into struct, %v", err)
+ }
return servers, nil
}
@@ -92,6 +101,6 @@ func loadServers(filename string) ([]serveractions.Server, error) {
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)
+ fmt.Printf(" - %s (%s) - %s - User: %s\n", s.Name, s.IP, s.Mac, s.SSHUser)
}
}
diff --git a/webserver/webserver.go b/webserver/webserver.go
index b4b5576..b9ea9ce 100644
--- a/webserver/webserver.go
+++ b/webserver/webserver.go
@@ -5,7 +5,9 @@ import (
"fmt"
"net/http"
- "manage-servers/server-actions"
+ serveractions "manage-servers/server-actions"
+
+ "github.com/spf13/viper"
)
type WebServer struct {
@@ -17,7 +19,7 @@ func StartWebServer(servers []serveractions.Server) {
fmt.Println("Starting web server on :8080...")
http.HandleFunc("/", ws.handleRoot)
- http.HandleFunc("/servers", ws.handleGetServers)
+ http.HandleFunc("/servers", ws.handleServers)
http.HandleFunc("/wake", ws.handleWakeServer)
http.HandleFunc("/shutdown", ws.handleShutdownServer)
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")
}
-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")
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) {
serverName := r.URL.Query().Get("name")
if serverName == "" {