diff options
Diffstat (limited to 'cmd/deploy/vps.go')
| -rw-r--r-- | cmd/deploy/vps.go | 229 |
1 files changed, 229 insertions, 0 deletions
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 @@ | |||
| 1 | package main | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "flag" | ||
| 5 | "fmt" | ||
| 6 | "os" | ||
| 7 | "os/exec" | ||
| 8 | |||
| 9 | "github.com/bdw/deploy/internal/ssh" | ||
| 10 | "github.com/bdw/deploy/internal/state" | ||
| 11 | ) | ||
| 12 | |||
| 13 | func runVPS(args []string) { | ||
| 14 | fs := flag.NewFlagSet("vps", flag.ExitOnError) | ||
| 15 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 16 | fs.Parse(args) | ||
| 17 | |||
| 18 | // Load state | ||
| 19 | st, err := state.Load() | ||
| 20 | if err != nil { | ||
| 21 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 22 | os.Exit(1) | ||
| 23 | } | ||
| 24 | |||
| 25 | // Get host from flag or state default | ||
| 26 | if *host == "" { | ||
| 27 | *host = st.GetDefaultHost() | ||
| 28 | } | ||
| 29 | |||
| 30 | if *host == "" { | ||
| 31 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 32 | fs.Usage() | ||
| 33 | os.Exit(1) | ||
| 34 | } | ||
| 35 | |||
| 36 | fmt.Printf("Connecting to %s...\n\n", *host) | ||
| 37 | |||
| 38 | // Connect to VPS | ||
| 39 | client, err := ssh.Connect(*host) | ||
| 40 | if err != nil { | ||
| 41 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 42 | os.Exit(1) | ||
| 43 | } | ||
| 44 | defer client.Close() | ||
| 45 | |||
| 46 | // Uptime | ||
| 47 | fmt.Println("UPTIME") | ||
| 48 | if output, err := client.Run("uptime -p"); err == nil { | ||
| 49 | fmt.Printf(" %s", output) | ||
| 50 | } | ||
| 51 | if output, err := client.Run("uptime -s"); err == nil { | ||
| 52 | fmt.Printf(" Since: %s", output) | ||
| 53 | } | ||
| 54 | fmt.Println() | ||
| 55 | |||
| 56 | // Load average | ||
| 57 | fmt.Println("LOAD") | ||
| 58 | if output, err := client.Run("cat /proc/loadavg | awk '{print $1, $2, $3}'"); err == nil { | ||
| 59 | fmt.Printf(" 1m, 5m, 15m: %s", output) | ||
| 60 | } | ||
| 61 | fmt.Println() | ||
| 62 | |||
| 63 | // Memory | ||
| 64 | fmt.Println("MEMORY") | ||
| 65 | if output, err := client.Run("free -h | awk 'NR==2 {print \" Used: \" $3 \" / \" $2}'"); err == nil { | ||
| 66 | fmt.Print(output) | ||
| 67 | } | ||
| 68 | if output, err := client.Run("free -h | awk 'NR==2 {printf \" Available: %s\\n\", $7}'"); err == nil { | ||
| 69 | fmt.Print(output) | ||
| 70 | } | ||
| 71 | fmt.Println() | ||
| 72 | |||
| 73 | // Disk | ||
| 74 | fmt.Println("DISK") | ||
| 75 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Used: \" $3 \" / \" $2 \" (\" $5 \")\"}'"); err == nil { | ||
| 76 | fmt.Print(output) | ||
| 77 | } | ||
| 78 | if output, err := client.Run("df -h / | awk 'NR==2 {print \" Available: \" $4}'"); err == nil { | ||
| 79 | fmt.Print(output) | ||
| 80 | } | ||
| 81 | fmt.Println() | ||
| 82 | |||
| 83 | // Updates available | ||
| 84 | fmt.Println("UPDATES") | ||
| 85 | 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 { | ||
| 86 | fmt.Print(output) | ||
| 87 | } | ||
| 88 | fmt.Println() | ||
| 89 | |||
| 90 | // Services | ||
| 91 | fmt.Println("SERVICES") | ||
| 92 | if output, err := client.Run("systemctl is-active caddy 2>/dev/null && echo ' Caddy: active' || echo ' Caddy: inactive'"); err == nil { | ||
| 93 | // Parse the output | ||
| 94 | if output == "active\n" { | ||
| 95 | fmt.Println(" Caddy: active") | ||
| 96 | } else { | ||
| 97 | fmt.Println(" Caddy: inactive") | ||
| 98 | } | ||
| 99 | } | ||
| 100 | |||
| 101 | // Count deployed apps that are running | ||
| 102 | hostState := st.GetHost(*host) | ||
| 103 | if hostState != nil && len(hostState.Apps) > 0 { | ||
| 104 | activeCount := 0 | ||
| 105 | for name, app := range hostState.Apps { | ||
| 106 | if app.Type == "app" { | ||
| 107 | if output, err := client.Run(fmt.Sprintf("systemctl is-active %s 2>/dev/null", name)); err == nil && output == "active\n" { | ||
| 108 | activeCount++ | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | appCount := 0 | ||
| 113 | for _, app := range hostState.Apps { | ||
| 114 | if app.Type == "app" { | ||
| 115 | appCount++ | ||
| 116 | } | ||
| 117 | } | ||
| 118 | fmt.Printf(" Deployed apps: %d/%d active\n", activeCount, appCount) | ||
| 119 | } | ||
| 120 | } | ||
| 121 | |||
| 122 | func runUpdate(args []string) { | ||
| 123 | fs := flag.NewFlagSet("vps-update", flag.ExitOnError) | ||
| 124 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 125 | yes := fs.Bool("y", false, "Skip confirmation prompt") | ||
| 126 | fs.Parse(args) | ||
| 127 | |||
| 128 | // Load state | ||
| 129 | st, err := state.Load() | ||
| 130 | if err != nil { | ||
| 131 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 132 | os.Exit(1) | ||
| 133 | } | ||
| 134 | |||
| 135 | // Get host from flag or state default | ||
| 136 | if *host == "" { | ||
| 137 | *host = st.GetDefaultHost() | ||
| 138 | } | ||
| 139 | |||
| 140 | if *host == "" { | ||
| 141 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 142 | fs.Usage() | ||
| 143 | os.Exit(1) | ||
| 144 | } | ||
| 145 | |||
| 146 | // Confirm unless -y flag is set | ||
| 147 | if !*yes { | ||
| 148 | fmt.Printf("This will run apt update && apt upgrade on %s\n", *host) | ||
| 149 | fmt.Print("Continue? [y/N]: ") | ||
| 150 | var response string | ||
| 151 | fmt.Scanln(&response) | ||
| 152 | if response != "y" && response != "Y" { | ||
| 153 | fmt.Println("Aborted.") | ||
| 154 | return | ||
| 155 | } | ||
| 156 | } | ||
| 157 | |||
| 158 | fmt.Printf("Connecting to %s...\n", *host) | ||
| 159 | |||
| 160 | // Connect to VPS | ||
| 161 | client, err := ssh.Connect(*host) | ||
| 162 | if err != nil { | ||
| 163 | fmt.Fprintf(os.Stderr, "Error connecting to VPS: %v\n", err) | ||
| 164 | os.Exit(1) | ||
| 165 | } | ||
| 166 | defer client.Close() | ||
| 167 | |||
| 168 | // Run apt update | ||
| 169 | fmt.Println("\nā Running apt update...") | ||
| 170 | if err := client.RunSudoStream("apt update"); err != nil { | ||
| 171 | fmt.Fprintf(os.Stderr, "Error running apt update: %v\n", err) | ||
| 172 | os.Exit(1) | ||
| 173 | } | ||
| 174 | |||
| 175 | // Run apt upgrade | ||
| 176 | fmt.Println("\nā Running apt upgrade...") | ||
| 177 | if err := client.RunSudoStream("DEBIAN_FRONTEND=noninteractive apt upgrade -y"); err != nil { | ||
| 178 | fmt.Fprintf(os.Stderr, "Error running apt upgrade: %v\n", err) | ||
| 179 | os.Exit(1) | ||
| 180 | } | ||
| 181 | |||
| 182 | // Check if reboot is required | ||
| 183 | fmt.Println() | ||
| 184 | if output, err := client.Run("[ -f /var/run/reboot-required ] && echo 'yes' || echo 'no'"); err == nil { | ||
| 185 | if output == "yes\n" { | ||
| 186 | fmt.Println("Note: A reboot is required to complete the update.") | ||
| 187 | } | ||
| 188 | } | ||
| 189 | |||
| 190 | fmt.Println("ā Update complete") | ||
| 191 | } | ||
| 192 | |||
| 193 | func runSSH(args []string) { | ||
| 194 | fs := flag.NewFlagSet("vps-ssh", flag.ExitOnError) | ||
| 195 | host := fs.String("host", "", "VPS host (SSH config alias or user@host)") | ||
| 196 | fs.Parse(args) | ||
| 197 | |||
| 198 | // Load state | ||
| 199 | st, err := state.Load() | ||
| 200 | if err != nil { | ||
| 201 | fmt.Fprintf(os.Stderr, "Error loading state: %v\n", err) | ||
| 202 | os.Exit(1) | ||
| 203 | } | ||
| 204 | |||
| 205 | // Get host from flag or state default | ||
| 206 | if *host == "" { | ||
| 207 | *host = st.GetDefaultHost() | ||
| 208 | } | ||
| 209 | |||
| 210 | if *host == "" { | ||
| 211 | fmt.Fprintf(os.Stderr, "Error: --host is required (no default host set)\n") | ||
| 212 | fs.Usage() | ||
| 213 | os.Exit(1) | ||
| 214 | } | ||
| 215 | |||
| 216 | // Launch interactive SSH session | ||
| 217 | cmd := exec.Command("ssh", *host) | ||
| 218 | cmd.Stdin = os.Stdin | ||
| 219 | cmd.Stdout = os.Stdout | ||
| 220 | cmd.Stderr = os.Stderr | ||
| 221 | |||
| 222 | if err := cmd.Run(); err != nil { | ||
| 223 | if exitErr, ok := err.(*exec.ExitError); ok { | ||
| 224 | os.Exit(exitErr.ExitCode()) | ||
| 225 | } | ||
| 226 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) | ||
| 227 | os.Exit(1) | ||
| 228 | } | ||
| 229 | } | ||
