added the base which is ai code and this will be tailored (and have been a little) for specifice yejayekhoob.com use case and tested extensively under short period of time
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dns-failover
|
||||||
27
bind.go
Normal file
27
bind.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SwitchBindConfig(source, target string) error {
|
||||||
|
input, err := os.ReadFile(source)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(target, input, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write active config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("rndc", "reload")
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reload bind: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
13
dns-failover.service
Normal file
13
dns-failover.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=DNS Failover Watchdog by db123
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/dns-failover -ip <ip> -v
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
User=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
151
main.go
Normal file
151
main.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user