152 lines
3.5 KiB
Go
152 lines
3.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
type Config struct {
|
|
HealthURL string
|
|
StaticIP string
|
|
NormalInterval time.Duration
|
|
FailInterval time.Duration
|
|
Timeout time.Duration
|
|
MaxRetries int
|
|
BindPrimaryConf string
|
|
BindFailoverConf string
|
|
BindActiveConf string
|
|
Verbose bool
|
|
}
|
|
|
|
type State int
|
|
|
|
const (
|
|
StatePrimary State = iota
|
|
StateFailover
|
|
)
|
|
|
|
func main() {
|
|
cfg := parseFlags()
|
|
|
|
logger := log.New(os.Stdout, "", log.LstdFlags)
|
|
if !cfg.Verbose {
|
|
logger.SetOutput(os.Stdout)
|
|
logger.SetFlags(0)
|
|
}
|
|
|
|
currentState := StatePrimary
|
|
failureCount := 0
|
|
successCount := 0
|
|
|
|
for {
|
|
healthy := checkHealth(cfg)
|
|
|
|
if currentState == StatePrimary {
|
|
if healthy {
|
|
failureCount = 0
|
|
sleep(cfg.NormalInterval)
|
|
continue
|
|
}
|
|
|
|
failureCount++
|
|
if cfg.Verbose {
|
|
logger.Println("Health check failed:", failureCount)
|
|
}
|
|
|
|
if failureCount >= cfg.MaxRetries {
|
|
logger.Println("Primary confirmed DOWN. Switching to failover.")
|
|
err := SwitchBindConfig(cfg.BindFailoverConf, cfg.BindActiveConf)
|
|
if err != nil {
|
|
logger.Println("Failed to switch BIND config:", err)
|
|
}
|
|
currentState = StateFailover
|
|
failureCount = 0
|
|
}
|
|
|
|
sleep(cfg.FailInterval)
|
|
continue
|
|
}
|
|
|
|
// FAILOVER MODE
|
|
if healthy {
|
|
successCount++
|
|
if cfg.Verbose {
|
|
logger.Println("Primary recovery attempt:", successCount)
|
|
}
|
|
|
|
if successCount >= cfg.MaxRetries {
|
|
logger.Println("Primary confirmed UP. Reverting DNS.")
|
|
err := SwitchBindConfig(cfg.BindPrimaryConf, cfg.BindActiveConf)
|
|
if err != nil {
|
|
logger.Println("Failed to revert BIND config:", err)
|
|
}
|
|
currentState = StatePrimary
|
|
successCount = 0
|
|
}
|
|
} else {
|
|
successCount = 0
|
|
}
|
|
|
|
sleep(cfg.NormalInterval)
|
|
}
|
|
}
|
|
|
|
func parseFlags() *Config {
|
|
cfg := &Config{}
|
|
|
|
flag.StringVar(&cfg.HealthURL, "url", "/nginx-health", "Health endpoint path")
|
|
flag.StringVar(&cfg.StaticIP, "ip", "", "Static IP to check")
|
|
flag.DurationVar(&cfg.NormalInterval, "interval", 120*time.Second, "Normal check interval")
|
|
flag.DurationVar(&cfg.FailInterval, "fail-interval", 10*time.Second, "Interval during failure detection")
|
|
flag.DurationVar(&cfg.Timeout, "timeout", 5*time.Second, "HTTP timeout")
|
|
flag.IntVar(&cfg.MaxRetries, "retries", 5, "Consecutive retries before state change")
|
|
flag.StringVar(&cfg.BindPrimaryConf, "primary-conf", "/etc/bind/db.primary", "Primary zone config")
|
|
flag.StringVar(&cfg.BindFailoverConf, "failover-conf", "/etc/bind/db.failover", "Failover zone config")
|
|
flag.StringVar(&cfg.BindActiveConf, "active-conf", "/etc/bind/db.active", "Active zone config path")
|
|
flag.BoolVar(&cfg.Verbose, "v", false, "Verbose logging")
|
|
|
|
flag.Parse()
|
|
|
|
if cfg.StaticIP == "" {
|
|
log.Fatal("Static IP must be provided")
|
|
}
|
|
|
|
return cfg
|
|
}
|
|
|
|
func checkHealth(cfg *Config) bool {
|
|
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
|
|
defer cancel()
|
|
|
|
dialer := &net.Dialer{}
|
|
transport := &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(cfg.StaticIP, "80"))
|
|
},
|
|
}
|
|
|
|
client := &http.Client{
|
|
Transport: transport,
|
|
Timeout: cfg.Timeout,
|
|
}
|
|
|
|
req, _ := http.NewRequestWithContext(ctx, "GET", "http://"+cfg.StaticIP+cfg.HealthURL, nil)
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return resp.StatusCode == 200
|
|
}
|
|
|
|
func sleep(d time.Duration) {
|
|
time.Sleep(d)
|
|
}
|