1 Commits

Author SHA1 Message Date
loveuer
4e66d187a7 feat: add install command for easy deployment
- Add `install` subcommand with alias `i`
- Support two installation methods: systemd (default) and service
- Copy binary to /usr/local/bin/go-alived
- Auto-detect network interface and hostname for config generation
- Create /etc/go-alived directory and config.yaml
- Install systemd service file with proper capabilities
- Display clear completion message with next steps
- Bump version to 1.2.0

🤖 Generated with [Qoder][https://qoder.com]
2026-03-04 00:54:08 -08:00
2 changed files with 348 additions and 1 deletions

347
internal/cmd/install.go Normal file
View File

@@ -0,0 +1,347 @@
package cmd
import (
"fmt"
"io"
"net"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
const (
defaultBinaryPath = "/usr/local/bin/go-alived"
defaultConfigDir = "/etc/go-alived"
defaultConfigFile = "/etc/go-alived/config.yaml"
systemdServicePath = "/etc/systemd/system/go-alived.service"
)
var (
installMethod string
)
var installCmd = &cobra.Command{
Use: "install",
Aliases: []string{"i"},
Short: "Install go-alived as a system service",
Long: `Install go-alived binary and configuration files to system paths.
Supported installation methods:
- systemd: Install as a systemd service (default, recommended for modern Linux)
- service: Install binary and config only (manual startup)
Examples:
sudo go-alived install
sudo go-alived install --method systemd
sudo go-alived i -m service`,
Run: runInstall,
}
func init() {
rootCmd.AddCommand(installCmd)
installCmd.Flags().StringVarP(&installMethod, "method", "m", "systemd",
"installation method: systemd, service")
}
func runInstall(cmd *cobra.Command, args []string) {
// Check root privileges
if os.Geteuid() != 0 {
fmt.Println("Error: This command requires root privileges")
fmt.Println("Please run with: sudo go-alived install")
os.Exit(1)
}
// Validate method
method := strings.ToLower(installMethod)
if method != "systemd" && method != "service" {
fmt.Printf("Error: Invalid installation method '%s'\n", installMethod)
fmt.Println("Supported methods: systemd, service")
os.Exit(1)
}
fmt.Println("=== Go-Alived Installation ===")
fmt.Println()
totalSteps := 2
if method == "systemd" {
totalSteps = 3
}
// Step 1: Copy binary
if err := installBinary(1, totalSteps); err != nil {
fmt.Printf("Error installing binary: %v\n", err)
os.Exit(1)
}
// Step 2: Create config directory and file
configCreated, err := installConfig(2, totalSteps)
if err != nil {
fmt.Printf("Error installing config: %v\n", err)
os.Exit(1)
}
// Step 3: Install systemd service if requested
if method == "systemd" {
if err := installSystemdService(3, totalSteps); err != nil {
fmt.Printf("Error installing systemd service: %v\n", err)
os.Exit(1)
}
}
// Print completion message
printCompletionMessage(method, configCreated)
}
func installBinary(step, total int) error {
fmt.Printf("[%d/%d] Installing binary... ", step, total)
// Get current executable path
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %w", err)
}
// Resolve symlinks
execPath, err = filepath.EvalSymlinks(execPath)
if err != nil {
return fmt.Errorf("failed to resolve symlinks: %w", err)
}
// Check if already installed at target path
if execPath == defaultBinaryPath {
fmt.Println("already installed")
return nil
}
// Open source file
src, err := os.Open(execPath)
if err != nil {
return fmt.Errorf("failed to open source binary: %w", err)
}
defer src.Close()
// Create destination file
dst, err := os.OpenFile(defaultBinaryPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return fmt.Errorf("failed to create destination binary: %w", err)
}
defer dst.Close()
// Copy binary
if _, err := io.Copy(dst, src); err != nil {
return fmt.Errorf("failed to copy binary: %w", err)
}
fmt.Printf("done (%s)\n", defaultBinaryPath)
return nil
}
func installConfig(step, total int) (bool, error) {
fmt.Printf("[%d/%d] Setting up configuration... ", step, total)
// Create config directory
if err := os.MkdirAll(defaultConfigDir, 0755); err != nil {
return false, fmt.Errorf("failed to create config directory: %w", err)
}
// Check if config file already exists
if _, err := os.Stat(defaultConfigFile); err == nil {
fmt.Println("config already exists")
return false, nil
}
// Generate config content
configContent := generateDefaultConfig()
// Write config file
if err := os.WriteFile(defaultConfigFile, []byte(configContent), 0644); err != nil {
return false, fmt.Errorf("failed to write config file: %w", err)
}
fmt.Printf("done (%s)\n", defaultConfigFile)
return true, nil
}
func installSystemdService(step, total int) error {
fmt.Printf("[%d/%d] Installing systemd service... ", step, total)
serviceContent := generateSystemdService()
if err := os.WriteFile(systemdServicePath, []byte(serviceContent), 0644); err != nil {
return fmt.Errorf("failed to write service file: %w", err)
}
fmt.Printf("done (%s)\n", systemdServicePath)
return nil
}
func generateDefaultConfig() string {
// Auto-detect network interface
iface := detectNetworkInterface()
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "node1"
}
return fmt.Sprintf(`# Go-Alived Configuration
# Generated by: go-alived install
# Documentation: https://github.com/loveuer/go-alived
global:
router_id: "%s"
vrrp_instances:
- name: "VI_1"
interface: "%s"
state: "BACKUP"
virtual_router_id: 51
priority: 100
advert_interval: 1
auth_type: "PASS"
auth_pass: "changeme" # TODO: Change this password
virtual_ips:
- "192.168.1.100/24" # TODO: Change to your VIP
# Optional: Health checkers
# health_checkers:
# - name: "check_nginx"
# type: "tcp"
# interval: 3s
# timeout: 2s
# rise: 3
# fall: 2
# config:
# host: "127.0.0.1"
# port: 80
`, hostname, iface)
}
func generateSystemdService() string {
return `[Unit]
Description=Go-Alived - VRRP High Availability Service
Documentation=https://github.com/loveuer/go-alived
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
ExecStart=/usr/local/bin/go-alived run --config /etc/go-alived/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=go-alived
# Security settings
NoNewPrivileges=false
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/etc/go-alived
# Resource limits
LimitNOFILE=65535
LimitNPROC=512
# Capabilities required for VRRP operations
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
`
}
func detectNetworkInterface() string {
interfaces, err := net.Interfaces()
if err != nil {
return "eth0"
}
for _, iface := range interfaces {
// Skip loopback and down interfaces
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue
}
// Check if interface has IPv4 address
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
if ipNet, ok := addr.(*net.IPNet); ok {
if ipv4 := ipNet.IP.To4(); ipv4 != nil && !ipv4.IsLoopback() {
return iface.Name
}
}
}
}
return "eth0"
}
func printCompletionMessage(method string, configCreated bool) {
fmt.Println()
fmt.Println("=== Installation Complete ===")
fmt.Println()
// What needs to be modified
fmt.Println(">>> Configuration Required:")
fmt.Printf(" Edit: %s\n", defaultConfigFile)
fmt.Println()
fmt.Println(" Modify the following settings:")
if configCreated {
fmt.Println(" - auth_pass: Change 'changeme' to a secure password")
fmt.Println(" - virtual_ips: Set your Virtual IP address(es)")
fmt.Println(" - interface: Verify the network interface is correct")
fmt.Println(" - priority: Adjust based on node role (higher = more likely to be master)")
} else {
fmt.Println(" - Review your existing configuration")
}
fmt.Println()
// How to start
fmt.Println(">>> Next Steps:")
if method == "systemd" {
fmt.Println(" 1. Edit configuration:")
fmt.Printf(" sudo vim %s\n", defaultConfigFile)
fmt.Println()
fmt.Println(" 2. Reload systemd and start service:")
fmt.Println(" sudo systemctl daemon-reload")
fmt.Println(" sudo systemctl enable go-alived")
fmt.Println(" sudo systemctl start go-alived")
fmt.Println()
fmt.Println(" 3. Check service status:")
fmt.Println(" sudo systemctl status go-alived")
fmt.Println(" sudo journalctl -u go-alived -f")
} else {
fmt.Println(" 1. Edit configuration:")
fmt.Printf(" sudo vim %s\n", defaultConfigFile)
fmt.Println()
fmt.Println(" 2. Run manually:")
fmt.Printf(" sudo %s run -c %s\n", defaultBinaryPath, defaultConfigFile)
fmt.Println()
fmt.Println(" 3. Or run in debug mode:")
fmt.Printf(" sudo %s run -c %s -d\n", defaultBinaryPath, defaultConfigFile)
}
fmt.Println()
// Test environment
fmt.Println(">>> Test Environment (Optional):")
fmt.Printf(" sudo %s test\n", defaultBinaryPath)
fmt.Println()
}

View File

@@ -11,7 +11,7 @@ var rootCmd = &cobra.Command{
Short: "Go-Alived - VRRP High Availability Service",
Long: `go-alived is a lightweight, dependency-free VRRP implementation in Go.
It provides high availability for IP addresses with health checking support.`,
Version: "1.0.0",
Version: "1.2.0",
}
func Execute() {