Files
edgeguard-native/internal/keepalived/keepalived_test.go
Debian d395e3ea68 fix(keepalived): REVERT preempt_delay → nopreempt (Prod-Ausfall 2026-08-03) — v1.3.15
v1.3.9 hatte auf dem PG-Primary (Prio 200) nopreempt durch preempt_delay ersetzt,
damit die VIP zum Primary heimwandert. Das reaktivierte GENAU den Fehlermodus,
den nopreempt verhindert: der Prio-200-Node holt die VIP zurück, sobald
keepalived ihn für gesund hält — aber die Track-Scripts können "gesund" melden,
während der Dienst kaputt ist. Am 2026-08-03 entriss so ein halb-kaputtes utm-1
dem funktionierenden utm-2 die VIP (Log: "Master received advert from .6 with
higher priority 200 → Entering BACKUP") und hielt sie fest → Ausfall, bis utm-1
hart abgeschaltet wurde.

Zurück auf nopreempt (beide Nodes, wie vor v1.3.9). VIP-Affinität zum Primary
erst wieder, wenn der Health-Check "Prozess up aber Dienst kaputt" als FAULT
erkennt. Bis dahin: Stabilität > Affinität.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 12:24:16 +02:00

116 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package keepalived
import (
"bytes"
"strings"
"testing"
"git.netcell-it.de/projekte/edgeguard-native/internal/models"
)
func render(t *testing.T, v View) string {
t.Helper()
var buf bytes.Buffer
if err := tpl.Execute(&buf, v); err != nil {
t.Fatalf("template execute: %v", err)
}
return buf.String()
}
func testView() View {
return View{
State: "BACKUP", Interface: "eth0", RouterID: 51, Priority: 200,
SrcIP: "89.163.205.6", PeerIP: "89.163.205.8", AuthPass: "edgeguard",
VIPs: []VIPEntry{{Address: "89.163.205.100", Prefix: 24, Device: "eth0"}},
HBInterface: "ens19", HBSrcIP: "169.254.0.1", HBPeerIP: "169.254.0.2", HBRouterID: 52,
GWCheckIP: "89.163.205.1",
}
}
// Split-Brain-Schutz (Incident 2026-08-03): BEIDE Instanzen MÜSSEN `nopreempt`
// tragen, sonst reißt ein erholter-aber-evtl-kaputter Node die VIP zurück.
// nopreempt wirkt nur bei state BACKUP — also darf kein Node MASTER starten.
func TestTemplateNopreemptOnBothInstances(t *testing.T) {
out := render(t, testView())
if n := strings.Count(out, "nopreempt"); n != 2 {
t.Fatalf("erwarte nopreempt in VI_1 UND VI_HB (2×), gefunden: %d\n%s", n, out)
}
if strings.Contains(out, "preempt_delay") {
t.Fatalf("KEIN preempt_delay erlaubt (Incident 2026-08-03):\n%s", out)
}
if strings.Contains(out, "state MASTER") {
t.Fatalf("kein Node darf state MASTER starten (nopreempt würde ignoriert):\n%s", out)
}
if c := strings.Count(out, "state BACKUP"); c != 2 {
t.Fatalf("erwarte state BACKUP in beiden Instanzen, gefunden: %d", c)
}
}
// GARP muss forciert + periodic aufgefrischt werden, sonst altert die
// VIP-MAC am Upstream-Switch und die Failover-IP wird unerreichbar.
func TestTemplateGARPRefresh(t *testing.T) {
out := render(t, testView())
for _, want := range []string{"vrrp_garp_master_refresh", "vrrp_garp_master_repeat"} {
if !strings.Contains(out, want) {
t.Fatalf("global_defs sollte %q enthalten:\n%s", want, out)
}
}
}
// advert_int 2 (statt 1): Master-Down ~6s — reißt nicht bei kurzen
// VM-/Heartbeat-Hiccups (Flapping-Schutz im virtualisierten Cluster).
func TestTemplateAdvertInt(t *testing.T) {
out := render(t, testView())
if strings.Contains(out, "advert_int 1\n") {
t.Fatalf("advert_int sollte 2 sein (nicht 1):\n%s", out)
}
if c := strings.Count(out, "advert_int 2"); c != 2 {
t.Fatalf("erwarte advert_int 2 in beiden Instanzen, gefunden: %d", c)
}
}
// Track-Scripts dürfen KEIN weight haben: in einer vrrp_sync_group ignoriert
// keepalived gewichtete Scripts → Health-Checks wären wirkungslos. Ohne weight
// wirken sie als FAULT-Trigger.
func TestTemplateTrackScriptsUnweighted(t *testing.T) {
out := render(t, testView())
if strings.Contains(out, "weight") {
t.Fatalf("Track-Scripts dürfen kein weight tragen (Sync-Group ignoriert sie sonst):\n%s", out)
}
}
// gw-Check darf nicht zu zucken (fall 5, nicht fall 2) — ein kurzer Upstream-
// Blip soll keinen Failover erzwingen.
func TestTemplateGatewayCheckNotTwitchy(t *testing.T) {
out := render(t, testView())
if !strings.Contains(out, "fall 5") {
t.Fatalf("chk_gateway sollte fall 5 nutzen:\n%s", out)
}
}
// buildView: State immer BACKUP, Priorität aus pg_role.
func TestBuildViewStateAlwaysBackup(t *testing.T) {
g := &generator{localID: "n1"}
cs := &models.ClusterSettings{VRRPRouterID: 51}
pub := "89.163.205.6"
cases := []struct {
pgRole, role string
wantPrio int
}{
{"primary", "primary", 200},
{"standby", "primary", 100},
{"", "primary", 200},
{"", "", 100},
}
for _, c := range cases {
local := &models.HANode{ID: "n1", PGRole: c.pgRole, Role: c.role, PublicIP: &pub}
v := g.buildView(cs, nil, local, nil)
if v.State != "BACKUP" {
t.Errorf("pg_role=%q role=%q: State=%q, erwarte immer BACKUP", c.pgRole, c.role, v.State)
}
if v.Priority != c.wantPrio {
t.Errorf("pg_role=%q role=%q: Priority=%d, erwarte %d", c.pgRole, c.role, v.Priority, c.wantPrio)
}
}
}