From 5f87d0accfabd792002de36cfb686030d7e80f6c Mon Sep 17 00:00:00 2001 From: Lorenzo Iovino Date: Mon, 21 Apr 2025 22:21:02 +0200 Subject: [PATCH] Initialize Go module --- .env | 4 + .env.sample | 4 + README.md | 255 +++++++++++++++++--------- go.mod | 5 + go.sum | 2 + handlers.go | 216 ++++++++++++++++++++++ main.go | 412 ++++-------------------------------------- templates/status.html | 402 +++++++++++++++++++++++++++++++++++++++++ utils.go | 100 ++++++++++ 9 files changed, 935 insertions(+), 465 deletions(-) create mode 100644 .env create mode 100644 .env.sample create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers.go create mode 100644 templates/status.html create mode 100644 utils.go diff --git a/.env b/.env new file mode 100644 index 0000000..6641729 --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +SERVER_NAME=delemaco +SERVER_USER=root +MAC_ADDRESS=b8:cb:29:a1:f3:88 +PORT=8080 diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..ea2a15a --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +SERVER_NAME=pippo +SERVER_USER=root +MAC_ADDRESS=aa:aa:aa:aa:aa:aa +PORT=8080 diff --git a/README.md b/README.md index 52eeac9..901f86d 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,91 @@ -![Server Status Screenshot](https://placeholder-for-screenshot.png) +# WOL-Server + +A lightweight Wake-on-LAN (WOL) server application designed specifically for Raspberry Pi devices. This tool allows you to remotely power on your network devices using magic packets through a simple, user-friendly interface. ## Features -- **Status Monitoring**: Real-time status checks to determine if your server is online or offline -- **Wake-on-LAN**: Boot your server remotely with the click of a button -- **Remote Shutdown**: Safely shut down your server when it's not needed -- **Responsive UI**: Simple, mobile-friendly interface with color-coded status indicators -- **Lightweight**: Built with Go for minimal resource usage, perfect for Raspberry Pi Zero - -## Requirements - -- Raspberry Pi (Zero, 2, 3, 4, etc.) -- Go (version 1.16 or higher) -- wakeonlan utility -- SSH access to the target server (for shutdown functionality) -- Target server configured for Wake-on-LAN +- **Cross-Platform Compatibility**: Optimized for various Raspberry Pi models (Zero, 1, 2, 3, 4) +- **Easy Installation**: Automated deployment script for quick setup +- **Systemd Integration**: Runs as a system service for reliability +- **Simple Web Interface**: Control your devices through an intuitive web UI +- **Configurable**: Easily customize settings through environment variables or `.env` file +- **Low Resource Usage**: Minimal footprint to run efficiently on any Pi ## Installation -### 1. Install Dependencies +### Method 1: Download Individual Files (Recommended) + +1. Download the necessary files directly to your Raspberry Pi: ```bash +# Create a directory for installation +mkdir -p ~/wol-install +cd ~/wol-install + +# Download the executable, service file, and deployment script +wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server-arm6 +wget https://github.com/thisloke/wol-server/releases/download/v6/wol-server.service +wget https://github.com/thisloke/wol-server/releases/download/v6/deploy.sh + +# Make files executable +chmod +x wol-server-arm6 +chmod +x deploy.sh +``` + +2. Create a `.env` file for configuration: +```bash +cat > .env << EOL +# Server Configuration +SERVER_NAME=pippo +SERVER_USER=root +MAC_ADDRESS=aa:aa:aa:aa:aa:aa +PORT=8080 +EOL +``` + +3. Run the deployment script: +```bash +./deploy.sh +``` + +### Method 2: Build from Source + +If you prefer to build the application from source: + +```bash +# Install Go (if not already installed) sudo apt update -sudo apt install golang-go wakeonlan -``` +sudo apt install golang-go -### 2. Clone the Repository - -```bash -git clone https://github.com/yourusername/wol-server.git +# Clone the repository +git clone https://github.com/thisloke/wol-server.git cd wol-server -``` -### 3. Configure the Application +# Install dependencies +go get github.com/joho/godotenv -Edit the constants in `main.go` to match your server: +# Create a .env file for configuration +cat > .env << EOL +# Server Configuration +SERVER_NAME=pippo +SERVER_USER=root +MAC_ADDRESS=aa:aa:aa:aa:aa:aa +PORT=8080 +EOL -```go -const ( - serverName = "yourserver" // Hostname or IP address of your server - macAddress = "xx:xx:xx:xx:xx:xx" // MAC address of your server's network interface - port = "8080" // Port to run the web application on -) -``` - -### 4. Build the Application - -```bash +# Build the application go build -o wol-server -``` -### 5. Set Up as a System Service +# Create installation directory +mkdir -p ~/wol-server -Create a systemd service file: +# Copy the binary and config +cp wol-server ~/wol-server/ +cp .env ~/wol-server/ +chmod +x ~/wol-server/wol-server -```bash -sudo nano /etc/systemd/system/wol-server.service -``` - -Add the following content (adjust paths if needed): - -``` +# Create and install systemd service +sudo bash -c 'cat > /etc/systemd/system/wol-server.service << EOL [Unit] Description=WOL Server Go Application After=network.target @@ -73,75 +98,129 @@ Restart=always [Install] WantedBy=multi-user.target -``` +EOL' -Enable and start the service: - -```bash +# Enable and start the service +sudo systemctl daemon-reload sudo systemctl enable wol-server sudo systemctl start wol-server ``` -## SSH Configuration for Remote Shutdown +## Configuration -For the shutdown functionality to work, you need to set up password-less SSH: +WOL-server can be configured using environment variables or a `.env` file in the application directory. -1. Generate an SSH key on your Raspberry Pi: - ```bash - ssh-keygen -t rsa - ``` +### Available Configuration Options -2. Copy the key to your target server: - ```bash - ssh-copy-id user@yourserver - ``` +| Environment Variable | Description | Default Value | +|----------------------|-------------|---------------| +| `SERVER_NAME` | Name of the server to ping/wake | `pippo` | +| `SERVER_USER` | SSH username for remote commands | `root` | +| `MAC_ADDRESS` | MAC address of the target server | `aa:aa:aa:aa:aa:aa` | +| `PORT` | The port number for the web server | `8080` | -3. Configure sudo on the target server to allow password-less shutdown: - ```bash - # On the target server, run: - sudo visudo +### Customizing Your Configuration - # Add this line (replacing 'user' with your username): - user ALL=(ALL) NOPASSWD: /sbin/shutdown - ``` +You can edit the `.env` file to modify the application's behavior: + +```bash +# Navigate to the installation directory +cd ~/wol-server + +# Edit the .env file +nano .env +``` + +After modifying the configuration, restart the service: + +```bash +sudo systemctl restart wol-server +``` ## Usage -Access the web interface by navigating to `http://raspberry-pi-ip:8080` in your browser. +Once installed, the WOL server will be accessible at: +``` +http://your-pi-ip:8080 +``` +(or whatever port you've configured) -The interface provides: +### Using the Web Interface -- Current server status (Online/Offline) -- Boot button - sends a Wake-on-LAN magic packet to your server -- Shutdown button - safely shuts down your server via SSH -- Refresh button - manually updates the status display +1. **Wake Server**: Click the "Boot" button to send a Wake-on-LAN magic packet to the configured MAC address +2. **Shut Down Server**: Click the "Shutdown" button, confirm, and enter your password if required + +## Service Management + +Control the WOL server using standard systemd commands: + +```bash +# Check service status +sudo systemctl status wol-server + +# Stop the service +sudo systemctl stop wol-server + +# Start the service +sudo systemctl start wol-server + +# Restart the service +sudo systemctl restart wol-server + +# View logs +journalctl -u wol-server -f +``` ## Troubleshooting -- **Server won't boot**: - - Verify that Wake-on-LAN is enabled in your server's BIOS/UEFI - - Confirm the MAC address is correct - - Check if your router blocks Wake-on-LAN packets +### Checking Your Raspberry Pi Architecture -- **Shutdown doesn't work**: - - Verify SSH key setup - - Check sudo configuration on target server - - Test manual SSH command: `ssh user@server sudo shutdown -h now` +If you need to verify which version of the application you should use: -- **Web interface not accessible**: - - Ensure the service is running: `sudo systemctl status wol-server` - - Check for firewall rules blocking port 8080 - - Verify the Raspberry Pi is connected to the network +```bash +uname -m +``` + +This will output your Pi's architecture: +- `armv6l`: Use the `wol-server-arm6` binary (Pi Zero, Pi 1) +- `armv7l`: Use the `wol-server-arm7` binary (Pi 2, Pi 3) +- `aarch64`: Use the `wol-server-arm64` binary (64-bit Pi 3, Pi 4) + +### Service Not Starting + +If the service doesn't start properly, check the logs: + +```bash +journalctl -u wol-server -e +``` + +### Checking Configuration + +Verify that your configuration is being properly loaded: + +```bash +# View the environment variables being used +sudo systemctl status wol-server +``` + +Look for a line in the output that shows the loaded configuration values. + +### Permission Issues + +Make sure the binary has execute permissions: + +```bash +chmod +x ~/wol-server/wol-server +``` ## Contributing -Contributions are welcome! Please feel free to submit a Pull Request. +Contributions are welcome! Feel free to submit pull requests or open issues for bugs and feature requests. ## License This project is licensed under the MIT License - see the LICENSE file for details. -## Acknowledgments +--- -- Inspired by the need for a simple, lightweight server power management tool -- Thanks to the Go community for the excellent standard library that makes web development straightforward +*WOL-Server - Simple Wake-on-LAN management for Raspberry Pi* diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c4afdd7 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/thisloke/wol-server + +go 1.20 + +require github.com/joho/godotenv v1.5.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d61b19e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..0c4333e --- /dev/null +++ b/handlers.go @@ -0,0 +1,216 @@ +package main + +import ( + "log" + "net/http" + "runtime" +) + +// Handle the root route - show status +func indexHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + + online := isServerOnline() + status := "Online" + color := "#4caf50" // Material green + if !online { + status = "Offline" + color = "#d32f2f" // Material red + } + + data := StatusData{ + Server: serverName, + Status: status, + Color: color, + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + ErrorMessage: "", + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} + +// Handle boot request +func bootHandler(w http.ResponseWriter, r *http.Request) { + if !isServerOnline() { + // Boot the server using wakeonlan + err := sendWakeOnLAN() + if err != nil { + log.Printf("Error booting server: %v", err) + } + + // Display booting status + data := StatusData{ + Server: serverName, + Status: "Booting", + Color: "#607d8b", // Material blue-gray + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + } else { + // Server is already online + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + } +} + +// Handle shutdown confirmation request +func confirmShutdownHandler(w http.ResponseWriter, r *http.Request) { + online := isServerOnline() + + if !online { + // Server is already offline + data := StatusData{ + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + return + } + + // Show confirmation dialog + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + ConfirmShutdown: true, + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} + +// Handle password entry for shutdown +func enterPasswordHandler(w http.ResponseWriter, r *http.Request) { + if !isServerOnline() { + // Server is already offline, redirect to home + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Show password entry dialog + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", // Material green + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } +} + +// Handle actual shutdown request +func shutdownHandler(w http.ResponseWriter, r *http.Request) { + // Only process POST requests for security + if r.Method != "POST" { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Parse form data to get password + if err := r.ParseForm(); err != nil { + log.Printf("Error parsing form: %v", err) + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + // Get password from form + password := r.FormValue("password") + + if password == "" { + // Show password form again with error + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Password cannot be empty", + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + return + } + + if isServerOnline() { + // Shutdown the server + err := shutdownServer(password) + if err != nil { + log.Printf("Error shutting down server: %v", err) + + // Show password form again with error + data := StatusData{ + Server: serverName, + Status: "Online", + Color: "#4caf50", + IsTestMode: runtime.GOOS == "darwin", + AskPassword: true, + ErrorMessage: "Failed to shutdown server. Please check your password.", + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + return + } + + // Display shutting down status + data := StatusData{ + Server: serverName, + Status: "Shutting down", + Color: "#5d4037", // Material brown + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + } else { + // Server is already offline + data := StatusData{ + Server: serverName, + Status: "Offline", + Color: "#d32f2f", // Material red + IsTestMode: runtime.GOOS == "darwin", + AskPassword: false, + } + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, "Failed to render template", http.StatusInternalServerError) + log.Printf("Template render error: %v", err) + } + } +} diff --git a/main.go b/main.go index affeb98..7bb50e8 100644 --- a/main.go +++ b/main.go @@ -2,405 +2,63 @@ package main import ( "fmt" - "html/template" "log" "net/http" - "os/exec" + "os" "runtime" + + "github.com/joho/godotenv" ) -const ( - serverName = "delemaco" // Server to ping - macAddress = "b8:cb:29:a1:f3:88" // MAC address of the server - port = "8080" // Port to listen on +// Default values +var ( + serverName = "server" // Server to ping + serverUser = "root" // SSH username + macAddress = "aa:aa:aa:aa:aa:aa" // MAC address of the server + port = "8080" // Port to listen on ) -// StatusData holds data for the HTML template -type StatusData struct { - Server string - Status string - Color string - IsTestMode bool -} - -// Check if server is online -func isServerOnline() bool { - var cmd *exec.Cmd - - // macOS and Linux have slightly different ping commands - if runtime.GOOS == "darwin" { - cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName) - } else { - cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName) +func loadEnvVariables() { + // Load .env file if it exists + if err := godotenv.Load(); err != nil { + log.Println("No .env file found, using default values") } - err := cmd.Run() - return err == nil -} - -// Send WOL packet -func sendWakeOnLAN() error { - log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress) - cmd := exec.Command("wakeonlan", macAddress) - return cmd.Run() -} - -// Shutdown server -func shutdownServer() error { - - // Real shutdown command for Linux - log.Printf("Sending shutdown command to %s", serverName) - cmd := exec.Command("ssh", serverName, "sudo", "shutdown", "-h", "now") - return cmd.Run() -} - -// Handle the root route - show status -func indexHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - http.NotFound(w, r) - return + // Override defaults with environment variables if they exist + if envServerName := os.Getenv("SERVER_NAME"); envServerName != "" { + serverName = envServerName } - online := isServerOnline() - status := "Online" - color := "#4caf50" // Material green - if !online { - status = "Offline" - color = "#d32f2f" // Material red + if envServerUser := os.Getenv("SERVER_USER"); envServerUser != "" { + serverUser = envServerUser } - data := StatusData{ - Server: serverName, - Status: status, - Color: color, - IsTestMode: runtime.GOOS == "darwin", + if envMacAddress := os.Getenv("MAC_ADDRESS"); envMacAddress != "" { + macAddress = envMacAddress } - renderTemplate(w, data) -} - -// Handle boot request -func bootHandler(w http.ResponseWriter, r *http.Request) { - if !isServerOnline() { - // Boot the server using wakeonlan - err := sendWakeOnLAN() - if err != nil { - log.Printf("Error booting server: %v", err) - } - - // Display booting status - data := StatusData{ - Server: serverName, - Status: "Booting", - Color: "#607d8b", // Material blue-gray - IsTestMode: runtime.GOOS == "darwin", - } - renderTemplate(w, data) - } else { - // Server is already online - data := StatusData{ - Server: serverName, - Status: "Online", - Color: "#4caf50", // Material green - IsTestMode: runtime.GOOS == "darwin", - } - renderTemplate(w, data) - } -} - -// Handle shutdown request -func shutdownHandler(w http.ResponseWriter, r *http.Request) { - if isServerOnline() { - // Shutdown the server - err := shutdownServer() - if err != nil { - log.Printf("Error shutting down server: %v", err) - } - - // Display shutting down status - data := StatusData{ - Server: serverName, - Status: "Shutting down", - Color: "#5d4037", // Material brown - IsTestMode: runtime.GOOS == "darwin", - } - renderTemplate(w, data) - } else { - // Server is already offline - data := StatusData{ - Server: serverName, - Status: "Offline", - Color: "#d32f2f", // Material red - IsTestMode: runtime.GOOS == "darwin", - } - renderTemplate(w, data) - } -} - -// Render the HTML template -func renderTemplate(w http.ResponseWriter, data StatusData) { - tmpl, err := template.New("status").Parse(` - - - - - Server Status: {{.Server}} - - - - - - -
-
-
-

{{.Status}}

-
Server: {{.Server}}
- - -
- - {{if .IsTestMode}} -
-
- Running on macOS in test mode. Wake-on-LAN packets are sent, but remote shutdown is simulated. -
-
- {{end}} - - -
- -`) - - if err != nil { - http.Error(w, "Failed to load template", http.StatusInternalServerError) - log.Printf("Template error: %v", err) - return + if envPort := os.Getenv("PORT"); envPort != "" { + port = envPort } - err = tmpl.Execute(w, data) - if err != nil { - http.Error(w, "Failed to render template", http.StatusInternalServerError) - log.Printf("Template render error: %v", err) - } + log.Printf("Configuration loaded: SERVER_NAME=%s, SERVER_USER=%s, MAC_ADDRESS=%s, PORT=%s", + serverName, serverUser, macAddress, port) } func main() { + // Load environment variables + loadEnvVariables() + + // Setup template + if err := setupTemplate(); err != nil { + log.Fatalf("Failed to setup template: %v", err) + } + // Register route handlers http.HandleFunc("/", indexHandler) http.HandleFunc("/boot", bootHandler) + http.HandleFunc("/confirm-shutdown", confirmShutdownHandler) + http.HandleFunc("/enter-password", enterPasswordHandler) http.HandleFunc("/shutdown", shutdownHandler) // Start the server @@ -408,7 +66,7 @@ func main() { log.Printf("Starting WOL Server on http://localhost%s", listenAddr) if runtime.GOOS == "darwin" { - log.Println("Running on macOS in test mode - remote shutdown commands will be simulated") + log.Println("Running on macOS - commands will be executed using the provided password") } if err := http.ListenAndServe(listenAddr, nil); err != nil { diff --git a/templates/status.html b/templates/status.html new file mode 100644 index 0000000..2c00eaa --- /dev/null +++ b/templates/status.html @@ -0,0 +1,402 @@ + + + + + + Server Status: {{.Server}} + + + + + + +
+
+
+

{{.Status}}

+
Server: {{.Server}}
+ + +
+ + {{if .IsTestMode}} +
+
+ Running on macOS. Commands will be executed using the provided password. +
+
+ {{end}} + + +
+ + {{if .ConfirmShutdown}} + + {{end}} + + {{if .AskPassword}} + + {{end}} + + diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..28e111b --- /dev/null +++ b/utils.go @@ -0,0 +1,100 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +// StatusData holds data for the HTML template +type StatusData struct { + Server string + Status string + Color string + IsTestMode bool + ConfirmShutdown bool + AskPassword bool + ErrorMessage string +} + +var tmpl *template.Template + +// Setup the HTML template +func setupTemplate() error { + // Check if templates directory exists, create if not + if _, err := os.Stat("templates"); os.IsNotExist(err) { + if err := os.Mkdir("templates", 0755); err != nil { + return fmt.Errorf("failed to create templates directory: %v", err) + } + } + + // Path to the template file + templatePath := filepath.Join("templates", "status.html") + + // Check if the template file exists + if _, err := os.Stat(templatePath); os.IsNotExist(err) { + log.Printf("Template file not found at %s. Please create it.", templatePath) + return fmt.Errorf("template file not found: %s", templatePath) + } + + // Parse the template from the file + var err error + tmpl, err = template.ParseFiles(templatePath) + if err != nil { + return fmt.Errorf("failed to parse template: %v", err) + } + + return nil +} + +// Check if server is online +func isServerOnline() bool { + var cmd *exec.Cmd + + // macOS and Linux have slightly different ping commands + if runtime.GOOS == "darwin" { + cmd = exec.Command("ping", "-c", "1", "-W", "1000", serverName) + } else { + cmd = exec.Command("ping", "-c", "1", "-W", "1", serverName) + } + + err := cmd.Run() + return err == nil +} + +// Send WOL packet +func sendWakeOnLAN() error { + log.Printf("Sending WOL packet to %s (%s)", serverName, macAddress) + cmd := exec.Command("wakeonlan", macAddress) + return cmd.Run() +} + +// Shutdown server with password +func shutdownServer(password string) error { + log.Printf("Sending shutdown command to %s", serverName) + + // Add more SSH options to handle potential issues + cmd := exec.Command("sshpass", "-p", password, "ssh", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=ERROR", + fmt.Sprintf("%s@%s", serverUser, serverName), + "sudo", "-S", "shutdown", "-h", "now") + + // Capture stderr to log any error messages + var stderr bytes.Buffer + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + log.Printf("SSH Error details: %s", stderr.String()) + return fmt.Errorf("SSH command failed: %v - %s", err, stderr.String()) + } + + return nil +}