Some checks failed
Release / Build darwin-amd64 (push) Has been cancelled
Release / Build linux-amd64 (push) Has been cancelled
Release / Build darwin-arm64 (push) Has been cancelled
Release / Build linux-arm64 (push) Has been cancelled
Release / Create Release (push) Has been cancelled
- Add GitHub Actions workflow for multi-platform releases - Build for linux/darwin on amd64/arm64 - Auto-create GitHub Release with checksums - Version injection via ldflags - Add init.d script support for install command - Rewrite README with clearer documentation - Quick start guide - Two-node HA setup example - Health check configuration - Troubleshooting section - Bump version to 1.2.1 🤖 Generated with [Qoder][https://qoder.com]
467 lines
12 KiB
Go
467 lines
12 KiB
Go
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"
|
|
initdScriptPath = "/etc/init.d/go-alived"
|
|
)
|
|
|
|
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 as a SysV init.d service (for older Linux distributions)
|
|
|
|
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()
|
|
|
|
const 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 service script
|
|
if err := installServiceScript(3, totalSteps, method); err != nil {
|
|
fmt.Printf("Error installing service script: %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 installServiceScript(step, total int, method string) error {
|
|
switch method {
|
|
case "systemd":
|
|
return installSystemdService(step, total)
|
|
case "service":
|
|
return installInitdScript(step, total)
|
|
default:
|
|
return fmt.Errorf("unsupported method: %s", method)
|
|
}
|
|
}
|
|
|
|
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 installInitdScript(step, total int) error {
|
|
fmt.Printf("[%d/%d] Installing init.d script... ", step, total)
|
|
|
|
scriptContent := generateInitdScript()
|
|
|
|
if err := os.WriteFile(initdScriptPath, []byte(scriptContent), 0755); err != nil {
|
|
return fmt.Errorf("failed to write init.d script: %w", err)
|
|
}
|
|
|
|
fmt.Printf("done (%s)\n", initdScriptPath)
|
|
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 generateInitdScript() string {
|
|
return `#!/bin/sh
|
|
### BEGIN INIT INFO
|
|
# Provides: go-alived
|
|
# Required-Start: $network $remote_fs $syslog
|
|
# Required-Stop: $network $remote_fs $syslog
|
|
# Default-Start: 2 3 4 5
|
|
# Default-Stop: 0 1 6
|
|
# Short-Description: Go-Alived VRRP High Availability Service
|
|
# Description: Lightweight VRRP implementation for IP high availability
|
|
### END INIT INFO
|
|
|
|
NAME="go-alived"
|
|
DAEMON="/usr/local/bin/go-alived"
|
|
DAEMON_ARGS="run --config /etc/go-alived/config.yaml"
|
|
PIDFILE="/var/run/${NAME}.pid"
|
|
LOGFILE="/var/log/${NAME}.log"
|
|
|
|
[ -x "$DAEMON" ] || exit 5
|
|
|
|
start() {
|
|
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
echo "$NAME is already running"
|
|
return 1
|
|
fi
|
|
echo -n "Starting $NAME... "
|
|
nohup $DAEMON $DAEMON_ARGS >> "$LOGFILE" 2>&1 &
|
|
echo $! > "$PIDFILE"
|
|
echo "done (PID: $(cat "$PIDFILE"))"
|
|
}
|
|
|
|
stop() {
|
|
if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
echo "$NAME is not running"
|
|
return 1
|
|
fi
|
|
echo -n "Stopping $NAME... "
|
|
kill "$(cat "$PIDFILE")"
|
|
rm -f "$PIDFILE"
|
|
echo "done"
|
|
}
|
|
|
|
restart() {
|
|
stop
|
|
sleep 1
|
|
start
|
|
}
|
|
|
|
reload() {
|
|
if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
echo "$NAME is not running"
|
|
return 1
|
|
fi
|
|
echo -n "Reloading $NAME configuration... "
|
|
kill -HUP "$(cat "$PIDFILE")"
|
|
echo "done"
|
|
}
|
|
|
|
status() {
|
|
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
|
|
echo "$NAME is running (PID: $(cat "$PIDFILE"))"
|
|
else
|
|
echo "$NAME is not running"
|
|
[ -f "$PIDFILE" ] && rm -f "$PIDFILE"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
case "$1" in
|
|
start) start ;;
|
|
stop) stop ;;
|
|
restart) restart ;;
|
|
reload) reload ;;
|
|
status) status ;;
|
|
*)
|
|
echo "Usage: $0 {start|stop|restart|reload|status}"
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
exit $?
|
|
`
|
|
}
|
|
|
|
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()
|
|
|
|
// Installed files summary
|
|
fmt.Println(">>> Installed Files:")
|
|
fmt.Printf(" Binary: %s\n", defaultBinaryPath)
|
|
fmt.Printf(" Config: %s\n", defaultConfigFile)
|
|
if method == "systemd" {
|
|
fmt.Printf(" Service: %s\n", systemdServicePath)
|
|
} else {
|
|
fmt.Printf(" Service: %s\n", initdScriptPath)
|
|
}
|
|
fmt.Println()
|
|
|
|
// What needs to be modified
|
|
fmt.Println(">>> Configuration Required:")
|
|
fmt.Printf(" Edit: %s\n", defaultConfigFile)
|
|
fmt.Println()
|
|
if configCreated {
|
|
fmt.Println(" Modify the following settings:")
|
|
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 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. Start service:")
|
|
fmt.Printf(" sudo %s start\n", initdScriptPath)
|
|
fmt.Println()
|
|
fmt.Println(" 3. Enable on boot (Debian/Ubuntu):")
|
|
fmt.Println(" sudo update-rc.d go-alived defaults")
|
|
fmt.Println()
|
|
fmt.Println(" 4. Check service status:")
|
|
fmt.Printf(" sudo %s status\n", initdScriptPath)
|
|
fmt.Printf(" tail -f /var/log/go-alived.log\n")
|
|
}
|
|
fmt.Println()
|
|
|
|
// Test environment
|
|
fmt.Println(">>> Test Environment (Optional):")
|
|
fmt.Printf(" sudo %s test\n", defaultBinaryPath)
|
|
fmt.Println()
|
|
}
|