From a34457f13f5d45450e1ddbcdaca5b3a8a7826310 Mon Sep 17 00:00:00 2001 From: noroot Date: Fri, 11 Sep 2026 12:15:19 +0200 Subject: [PATCH] fix(ui): Cluster-Karte zeigte auf dem Standby beide Knoten als DB-Primary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auf utm-2 trug die Cluster-Karte an BEIDEN Knoten "DB-Primary" — auf utm-1 dagegen korrekt "primary" und "peer". Es kann aber nur einen DB-Primary geben, also war mindestens eine der beiden Ansichten falsch. Ursache: ha_nodes ist NICHT repliziert (node-lokale Tabelle, jeder Knoten fuehrt seine eigene Sicht), und in `role` traegt sich jeder Knoten selbst ein. Ein per Join dazugekommener Knoten behaelt dort den Default "primary" — utm-2 behauptete in seiner eigenen Ansicht also, es sei Primary. Der Code weiss das an anderer Stelle bereits: cluster_repair.go dokumentiert "role/pg_role sind je Node lokal und unzuverlaessig", und keepalived.go gibt pg_role deshalb seit v1.2.46 absoluten Vorrang vor role. Die Karte zeigt jetzt pg_role statt role. Das folgt der tatsaechlichen Replikationsrolle und stimmt auf beiden Seiten ueberein (utm-1=primary, utm-2=standby). 'standalone'/leer heisst "keine Replikation eingerichtet" und zeigt bewusst gar keine Rolle, statt eine zu erfinden. Die stale role='primary'-Zeile auf utm-2 wurde separat direkt in der node-lokalen DB korrigiert. Sie war nicht nur kosmetisch: faellt pg_role irgendwann aus (leer/'standalone'), greift in keepalived.go der role-Fallback — und genau diese Konstellation hat 2026-05 schon einmal einen Split-Brain ausgeloest (beide Knoten Prio 200, hoehere IP gewinnt). Co-Authored-By: Claude Opus 5 --- management-ui/src/i18n/locales/de/common.json | 5 ++- management-ui/src/i18n/locales/en/common.json | 5 ++- management-ui/src/pages/Dashboard/index.tsx | 41 ++++++++++++++----- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/management-ui/src/i18n/locales/de/common.json b/management-ui/src/i18n/locales/de/common.json index 9ae7987..ceec102 100644 --- a/management-ui/src/i18n/locales/de/common.json +++ b/management-ui/src/i18n/locales/de/common.json @@ -424,8 +424,9 @@ "split-brain": "split-brain" }, "roleDbPrimary": "DB-Primary", - "rolePeer": "Peer", - "roleHint": "DB-/Cluster-Rolle: bestimmt, wohin Schreibzugriffe gehen. Sie wandert NICHT mit der VIP — ein Node kann DB-Primary sein und trotzdem gerade keepalived-BACKUP (siehe VIP/VRRP-Karte). Sie aendert sich nur durch „edgeguard-ctl promote“." + "roleHint": "Datenbank-Rolle aus der Replikation: DB-Primary nimmt Schreibzugriffe entgegen, DB-Standby repliziert von dort. Sie wandert NICHT mit der VIP — ein Knoten kann DB-Primary sein und trotzdem gerade keepalived-BACKUP (siehe VIP/VRRP-Karte). Sie aendert sich nur durch „edgeguard-ctl promote“.", + "roleDbStandby": "DB-Standby", + "roleDbUnknown": "—" }, "routingCard": { "title": "Routing", diff --git a/management-ui/src/i18n/locales/en/common.json b/management-ui/src/i18n/locales/en/common.json index f564d4e..382d6d4 100644 --- a/management-ui/src/i18n/locales/en/common.json +++ b/management-ui/src/i18n/locales/en/common.json @@ -424,8 +424,9 @@ "split-brain": "split-brain" }, "roleDbPrimary": "DB primary", - "rolePeer": "Peer", - "roleHint": "Database/cluster role: decides where writes go. It does NOT follow the VIP — a node can be DB primary while currently being keepalived BACKUP (see the VIP/VRRP card). It only changes via \"edgeguard-ctl promote\"." + "roleHint": "Database role from replication: the DB primary accepts writes, the DB standby replicates from it. It does NOT follow the VIP — a node can be DB primary while currently being keepalived BACKUP (see the VIP/VRRP card). It only changes via \"edgeguard-ctl promote\".", + "roleDbStandby": "DB standby", + "roleDbUnknown": "—" }, "routingCard": { "title": "Routing", diff --git a/management-ui/src/pages/Dashboard/index.tsx b/management-ui/src/pages/Dashboard/index.tsx index fb1075c..c7edae5 100644 --- a/management-ui/src/pages/Dashboard/index.tsx +++ b/management-ui/src/pages/Dashboard/index.tsx @@ -68,7 +68,7 @@ interface FwRule { id: number; enabled: boolean; action: string } interface FwNAT { id: number; enabled: boolean; kind: string } interface FwZone { id: number; name: string; builtin: boolean } interface TLSCert { id: number; common_name: string; not_after?: string } -interface ClusterNode { id: string; fqdn: string; role: string } +interface ClusterNode { id: string; fqdn: string; role: string; pg_role?: string } interface WGIface { id: number; name: string; mode: string; active: boolean } interface WGStatusRow { interface: string @@ -644,6 +644,24 @@ function VIPCard({ data }: { data?: VIPStatus | null }) { // ── Cluster card ────────────────────────────────────────────── +// dbRoleLabel/-Color bilden pg_role ab. 'standalone' bzw. leer heisst: +// keine Replikation eingerichtet — dann gibt es schlicht keine DB-Rolle +// zu zeigen, statt eine zu erfinden. +function dbRoleLabel(pgRole: string | undefined, t: (k: string) => string): string { + switch (pgRole) { + case 'primary': return t('dashboard.clusterCard.roleDbPrimary') + case 'standby': return t('dashboard.clusterCard.roleDbStandby') + default: return t('dashboard.clusterCard.roleDbUnknown') + } +} +function dbRoleColor(pgRole: string | undefined): string { + switch (pgRole) { + case 'primary': return 'green' + case 'standby': return 'blue' + default: return 'default' + } +} + interface ClusterStatusCardProps { nodes: ClusterNode[] status: { mode: string; health: string; drift_found: boolean } | null @@ -677,16 +695,19 @@ function ClusterStatusCard({ nodes, status }: ClusterStatusCardProps) { padding: '4px 0', borderBottom: '1px solid #F1F5F9', fontSize: 12, }}> {n.fqdn} - {/* ha_nodes.role ist die DB-/Cluster-Rolle (wohin Schreibzugriffe - gehen) und wandert bewusst NICHT mit der VIP — sie aendert - sich nur durch `edgeguard-ctl promote`. Ein nacktes "primary" - hier las sich neben der VIP-Karte ("BACKUP") wie ein - Widerspruch, deshalb explizit als DB-Rolle beschriftet. */} + {/* Bewusst pg_role, NICHT role: ha_nodes ist node-lokal (nicht + repliziert), und jeder Node traegt sich in `role` selbst ein — + ein per Join dazugekommener Node behaelt dort den Default + "primary" und behauptete deshalb in seiner eigenen Ansicht, + beide Knoten seien DB-Primary. `pg_role` folgt der + tatsaechlichen Replikationsrolle und stimmt auf beiden Seiten + ueberein. Siehe auch cluster_repair.go: role/pg_role sind + node-lokal, verlaesslich ist letztlich die PUBLICATION. + Die DB-Rolle wandert NICHT mit der VIP — sie aendert sich nur + durch `edgeguard-ctl promote`. */} - - {n.role === 'primary' - ? t('dashboard.clusterCard.roleDbPrimary') - : t('dashboard.clusterCard.rolePeer')} + + {dbRoleLabel(n.pg_role, t)}