Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e66d187a7 |
347
internal/cmd/install.go
Normal file
347
internal/cmd/install.go
Normal 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()
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user