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",
|
Short: "Go-Alived - VRRP High Availability Service",
|
||||||
Long: `go-alived is a lightweight, dependency-free VRRP implementation in Go.
|
Long: `go-alived is a lightweight, dependency-free VRRP implementation in Go.
|
||||||
It provides high availability for IP addresses with health checking support.`,
|
It provides high availability for IP addresses with health checking support.`,
|
||||||
Version: "1.0.0",
|
Version: "1.2.0",
|
||||||
}
|
}
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
|
|||||||
Reference in New Issue
Block a user