Files
dns-failover/main.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)
}