package main import ( "context" "fmt" "os" "strings" "time" "git.netcell-it.de/projekte/edgeguard-native/internal/cluster" "git.netcell-it.de/projekte/edgeguard-native/internal/database" "git.netcell-it.de/projekte/edgeguard-native/internal/keepalived" ) // cmdPromote befördert diese Node zum Logical-Replication-Primary. Manuelles // Failover — keine automatische Promotion, um Split-Brain in 2-Node-Clustern // ohne externes Quorum zu verhindern. // // Hintergrund: Die Replikation ist LOGICAL (Publication/Subscription), nicht // physisch. Ein Subscriber ist eine normale beschreibbare PG-Instanz (nie „in // recovery", kein standby.signal). „Promote" heißt darum: Subscription zum // (toten/alten) Primary lösen und selbst Publisher werden. // // Ablauf: // 1. Idempotenz-Check: schon Publisher ohne Subscription → fertig // 2. Subscription lösen (DISABLE + slot_name=NONE + DROP) // 3. setupReplicationPrimary: Rolle/Secret/conf.d/pg_hba/Grants/Publication // + sicherstellen dass wal_level=logical aktiv ist (PG-Restart falls nötig) // 4. ha_nodes.pg_role/role = 'primary' // 5. keepalived neu rendern (Primary = Priorität 200 = MASTER → übernimmt VIP) func cmdPromote(args []string) int { pg, err := detectPGConfig() if err != nil { fmt.Fprintln(os.Stderr, "promote: PG-Erkennung:", err) return 1 } // 1. Idempotenz: bereits Publisher (Primary) ohne Subscription? pubOut, _ := psqlDBRun("edgeguard", []string{"-tA", "-c", fmt.Sprintf("SELECT count(*) FROM pg_publication WHERE pubname='%s';", egPubName)}) subOut, _ := psqlDBRun("edgeguard", []string{"-tA", "-c", fmt.Sprintf("SELECT count(*) FROM pg_subscription WHERE subname='%s';", egSubName)}) hasPub := strings.TrimSpace(string(pubOut)) == "1" hasSub := strings.TrimSpace(string(subOut)) == "1" if hasPub && !hasSub { fmt.Println("✓ Diese Node ist bereits Logical-Replication-Primary (Publication vorhanden, keine Subscription). Nichts zu tun.") return 0 } fmt.Printf("→ Promote zu Logical-Replication-Primary (PostgreSQL %s/%s)...\n", pg.Version, pg.Cluster) // 2. Subscription zum alten/toten Primary lösen if hasSub { if err := dropSubscriptionIfExists(); err != nil { fmt.Fprintln(os.Stderr, "promote: Subscription lösen:", err) return 1 } fmt.Println("✓ Subscription zum alten Primary entfernt") } // 3. Diese Node als Publisher einrichten (inkl. wal_level=logical + Restart) if err := setupReplicationPrimary(pg); err != nil { fmt.Fprintln(os.Stderr, "promote:", err) return 1 } // 4. ha_nodes-Rolle aktualisieren ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() pool, err := database.Open(ctx, database.ConnStringFromEnv()) if err != nil { fmt.Fprintln(os.Stderr, "promote: db connect:", err) fmt.Println(" → ha_nodes manuell: UPDATE ha_nodes SET pg_role='primary', role='primary' WHERE id='';") } else { defer pool.Close() localID, err := loadLocalID() if err != nil { fmt.Fprintln(os.Stderr, "promote: local node ID:", err) } else { _, err = pool.Exec(ctx, `UPDATE ha_nodes SET pg_role='primary', role='primary', status='online', updated_at=NOW() WHERE id=$1`, localID) if err != nil { fmt.Fprintln(os.Stderr, "promote: update ha_nodes:", err) } else { fmt.Println("✓ ha_nodes.pg_role = 'primary' gesetzt") } } } // 5. keepalived.conf neu rendern (Primary = MASTER, Priority 200 → VIP) if pool != nil { localID, _ := loadLocalID() kg := keepalived.New(pool, localID) renderCtx, renderCancel := context.WithTimeout(context.Background(), 10*time.Second) defer renderCancel() if err := kg.Render(renderCtx); err != nil { fmt.Fprintf(os.Stderr, "promote: keepalived render: %v\n", err) fmt.Println(" → Manuell: sudo -u edgeguard edgeguard-ctl render-config --only=keepalived") } else { fmt.Println("✓ keepalived.conf neu gerendert (MASTER, Priority 200)") } } fmt.Println() fmt.Println("✓ Promotion abgeschlossen. Diese Node ist jetzt der primäre EdgeGuard-Knoten.") fmt.Println() fmt.Println("Empfohlene Nachschritte:") fmt.Println(" 1) sudo systemctl restart edgeguard-api") fmt.Println(" 2) Übrige/erholte Nodes als Standby auf DIESE Node zeigen lassen:") fmt.Println(" edgeguard-ctl cluster-setup-standby ") return 0 } // loadLocalID liest die Node-ID aus /var/lib/edgeguard/node.conf. func loadLocalID() (string, error) { c, err := cluster.LoadLocalConfig("") if err != nil { return "", err } if c.NodeID == "" { return "", fmt.Errorf("NODE_ID in node.conf ist leer") } return c.NodeID, nil }