From 57eb67df265a7a6bb544cde83a3be5eadf53fdf2 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 21:39:19 -0800 Subject: Add VPS management commands: vps, vps-update, vps-ssh - vps: Show server health (uptime, load, memory, disk, services) - vps-update: Run apt update && upgrade with streaming output - vps-ssh: Open interactive SSH session to default/specified host --- cmd/deploy/main.go | 15 ++++ cmd/deploy/vps.go | 229 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 cmd/deploy/vps.go diff --git a/cmd/deploy/main.go b/cmd/deploy/main.go index e86eff7..51439dd 100644 --- a/cmd/deploy/main.go +++ b/cmd/deploy/main.go @@ -30,6 +30,12 @@ func main() { runEnv(os.Args[2:]) case "webui": runWebUI(os.Args[2:]) + case "vps": + runVPS(os.Args[2:]) + case "vps-update": + runUpdate(os.Args[2:]) + case "vps-ssh": + runSSH(os.Args[2:]) case "help", "--help", "-h": printUsage() default: @@ -53,6 +59,9 @@ COMMANDS: status Check status of a deployment restart Restart a deployment env Manage environment variables + vps Show VPS health (uptime, disk, memory, load) + vps-update Update VPS packages (apt update && upgrade) + vps-ssh Open an interactive SSH session webui Launch web UI to manage deployments FLAGS: @@ -73,6 +82,12 @@ EXAMPLES: # View logs deploy logs myapp + + # Check VPS health + deploy vps + + # Update VPS packages + deploy vps-update ` fmt.Fprint(os.Stderr, usage) } diff --git a/cmd/deploy/vps.go b/cmd/deploy/vps.go new file mode 100644 index 0000000..bd6278b --- /dev/null +++ b/cmd/deploy/vps.go @@ -0,0 +1,229 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + + "github.com/bdw/deploy/internal/ssh" + "github.com/bdw/deploy/internal/state" +) + +func runVPS(args []string) { + fs := flag.NewFlagSet("vps", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + fs.Parse(args) + + // Load state + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + // Get host from flag or state default + if *host == "" { + *host = st.GetDefaultHost() + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") + fs.Usage() + os.Exit(1) + } + + fmt.Printf("Connecting to %s...\n\n", *host) + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + // Uptime + fmt.Println("UPTIME") + if output, err := client.Run("uptime -p"); err == nil { + fmt.Printf(" %s", output) + } + if output, err := client.Run("uptime -s"); err == nil { + fmt.Printf(" Since: %s", output) + } + fmt.Println() + + // Load average + fmt.Println("LOAD") + if output, err := client.Run("cat /proc/loadavg | awk '{print $1, $2, $3}'"); err == nil { + fmt.Printf(" 1m, 5m, 15m: %s", output) + } + fmt.Println() + + // Memory + fmt.Println("MEMORY") + if output, err := client.Run("free -h | awk 'NR==2 {print \" Used: \" $3 \" / \" $2}'"); err == nil { + fmt.Print(output) + } + if output, err := client.Run("free -h | awk 'NR==2 {printf \" Available: %s\\n\", $7}'"); err == nil { + fmt.Print(output) + } + fmt.Println() + + // Disk + fmt.Println("DISK") + if output, err := client.Run("df -h / | awk 'NR==2 {print \" Used: \" $3 \" / \" $2 \" (\" $5 \")\"}'"); err == nil { + fmt.Print(output) + } + if output, err := client.Run("df -h / | awk 'NR==2 {print \" Available: \" $4}'"); err == nil { + fmt.Print(output) + } + fmt.Println() + + // Updates available + fmt.Println("UPDATES") + if output, err := client.Run("[ -f /var/lib/update-notifier/updates-available ] && cat /var/lib/update-notifier/updates-available | head -2 || echo ' (update info not available)'"); err == nil { + fmt.Print(output) + } + fmt.Println() + + // Services + fmt.Println("SERVICES") + if output, err := client.Run("systemctl is-active caddy 2>/dev/null && echo ' Caddy: active' || echo ' Caddy: inactive'"); err == nil { + // Parse the output + if output == "active\n" { + fmt.Println(" Caddy: active") + } else { + fmt.Println(" Caddy: inactive") + } + } + + // Count deployed apps that are running + hostState := st.GetHost(*host) + if hostState != nil && len(hostState.Apps) > 0 { + activeCount := 0 + for name, app := range hostState.Apps { + if app.Type == "app" { + if output, err := client.Run(fmt.Sprintf("systemctl is-active %s 2>/dev/null", name)); err == nil && output == "active\n" { + activeCount++ + } + } + } + appCount := 0 + for _, app := range hostState.Apps { + if app.Type == "app" { + appCount++ + } + } + fmt.Printf(" Deployed apps: %d/%d active\n", activeCount, appCount) + } +} + +func runUpdate(args []string) { + fs := flag.NewFlagSet("vps-update", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + yes := fs.Bool("y", false, "Skip confirmation prompt") + fs.Parse(args) + + // Load state + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + // Get host from flag or state default + if *host == "" { + *host = st.GetDefaultHost() + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") + fs.Usage() + os.Exit(1) + } + + // Confirm unless -y flag is set + if !*yes { + fmt.Printf("This will run apt update && apt upgrade on %s\n", *host) + fmt.Print("Continue? [y/N]: ") + var response string + fmt.Scanln(&response) + if response != "y" && response != "Y" { + fmt.Println("Aborted.") + return + } + } + + fmt.Printf("Connecting to %s...\n", *host) + + // Connect to VPS + client, err := ssh.Connect(*host) + if err != nil { + fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) + os.Exit(1) + } + defer client.Close() + + // Run apt update + fmt.Println("\n→ Running apt update...") + if err := client.RunSudoStream("apt update"); err != nil { + fmt.Fprintf(os.Stderr, "Error running apt update: %v\n", err) + os.Exit(1) + } + + // Run apt upgrade + fmt.Println("\n→ Running apt upgrade...") + if err := client.RunSudoStream("DEBIAN_FRONTEND=noninteractive apt upgrade -y"); err != nil { + fmt.Fprintf(os.Stderr, "Error running apt upgrade: %v\n", err) + os.Exit(1) + } + + // Check if reboot is required + fmt.Println() + if output, err := client.Run("[ -f /var/run/reboot-required ] && echo 'yes' || echo 'no'"); err == nil { + if output == "yes\n" { + fmt.Println("Note: A reboot is required to complete the update.") + } + } + + fmt.Println("āœ“ Update complete") +} + +func runSSH(args []string) { + fs := flag.NewFlagSet("vps-ssh", flag.ExitOnError) + host := fs.String("host", "", "VPS host (SSH config alias or user@host)") + fs.Parse(args) + + // Load state + st, err := state.Load() + if err != nil { + fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) + os.Exit(1) + } + + // Get host from flag or state default + if *host == "" { + *host = st.GetDefaultHost() + } + + if *host == "" { + fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") + fs.Usage() + os.Exit(1) + } + + // Launch interactive SSH session + cmd := exec.Command("ssh", *host) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} -- cgit v1.2.3