Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6149670375 | ||
|
|
1b13df4032 | ||
|
|
3ca37ee226 | ||
|
|
86aee33308 |
@@ -168,7 +168,8 @@ func main() {
|
|||||||
if st != nil && st.Completed {
|
if st != nil && st.Completed {
|
||||||
// Auto-create /etc/edgeguard/node.conf falls fehlt.
|
// Auto-create /etc/edgeguard/node.conf falls fehlt.
|
||||||
_, _ = cluster.EnsureLocalConfig("")
|
_, _ = cluster.EnsureLocalConfig("")
|
||||||
if _, err := cluster.EnsureSelfRegistered(ctx, clusterStore, st.FQDN, "primary", version); err != nil {
|
if _, err := cluster.EnsureSelfRegistered(ctx, clusterStore, st.FQDN,
|
||||||
|
localClusterRole(ctx, pool, st), version); err != nil {
|
||||||
slog.Warn("self-register in ha_nodes failed", "error", err)
|
slog.Warn("self-register in ha_nodes failed", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,12 +304,12 @@ func main() {
|
|||||||
systemHdl.WithAudit(auditRepo, nodeID)
|
systemHdl.WithAudit(auditRepo, nodeID)
|
||||||
systemHdl.WithDB(pool)
|
systemHdl.WithDB(pool)
|
||||||
systemHdl.WithConfigPreviewers(map[string]func(context.Context) (string, error){
|
systemHdl.WithConfigPreviewers(map[string]func(context.Context) (string, error){
|
||||||
"haproxy": haproxy.New(pool).RenderToString,
|
"haproxy": haproxy.New(pool).RenderToString,
|
||||||
"nftables": firewallrender.New(pool).RenderToString,
|
"nftables": firewallrender.New(pool).RenderToString,
|
||||||
"squid": squidrender.New(pool).RenderToString,
|
"squid": squidrender.New(pool).RenderToString,
|
||||||
"unbound": unboundrender.New(pool).RenderToString,
|
"unbound": unboundrender.New(pool).RenderToString,
|
||||||
"chrony": chronyrender.New(pool).RenderToString,
|
"chrony": chronyrender.New(pool).RenderToString,
|
||||||
"wireguard": wgrender.New(pool, secretsBox).RenderToString,
|
"wireguard": wgrender.New(pool, secretsBox).RenderToString,
|
||||||
"crowdsec-whitelist": crowdsec.NewWhitelistGenerator(pool).RenderToString,
|
"crowdsec-whitelist": crowdsec.NewWhitelistGenerator(pool).RenderToString,
|
||||||
})
|
})
|
||||||
setupHdl.WithAudit(auditRepo, nodeID)
|
setupHdl.WithAudit(auditRepo, nodeID)
|
||||||
@@ -927,3 +928,47 @@ func randomEphemeralSecret() []byte {
|
|||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// localClusterRole ermittelt die eigene Cluster-Rolle für die node-lokale
|
||||||
|
// ha_nodes-Zeile.
|
||||||
|
//
|
||||||
|
// Befund 2026-09-11: Hier stand fest "primary" — für JEDEN Node, bei jedem
|
||||||
|
// API-Start. ha_nodes ist node-lokal (nicht repliziert), also trug sich auch
|
||||||
|
// ein per Join dazugekommener Standby bei sich selbst als "primary" ein. In
|
||||||
|
// der Cluster-Ansicht DIESES Nodes erschienen dadurch beide Knoten als
|
||||||
|
// Primary, und eine Korrektur direkt in der DB hielt nur bis zum nächsten
|
||||||
|
// Neustart.
|
||||||
|
//
|
||||||
|
// Nicht kosmetisch: keepalived.go nutzt `role` als Fallback, wenn pg_role
|
||||||
|
// nicht 'standby' ist. Ein Standby, der sich selbst "primary" nennt, ist
|
||||||
|
// damit genau der Zustand, der 2026-05 schon einmal einen Split-Brain
|
||||||
|
// ausgelöst hat (beide Knoten Priorität 200, höhere IP gewinnt).
|
||||||
|
//
|
||||||
|
// Verlässlich ist — wie in cluster_repair.go dokumentiert — die
|
||||||
|
// Replikations-Topologie selbst: nur der Primary hat die PUBLICATION, nur
|
||||||
|
// der Standby die SUBSCRIPTION. Beide Kataloge darf der edgeguard-DB-User
|
||||||
|
// lesen. Das ist zugleich selbstheilend: nach `edgeguard-ctl promote` hat
|
||||||
|
// der neue Primary die Publication und meldet sich ab dem nächsten Start
|
||||||
|
// korrekt als "primary" — anders als eine Ableitung aus setup.json, die
|
||||||
|
// den Promote überschreiben würde.
|
||||||
|
func localClusterRole(ctx context.Context, pool *pgxpool.Pool, st *setup.State) string {
|
||||||
|
if pool != nil {
|
||||||
|
var hasPub, hasSub bool
|
||||||
|
if err := pool.QueryRow(ctx,
|
||||||
|
`SELECT EXISTS(SELECT 1 FROM pg_publication WHERE pubname = 'edgeguard_shared')`,
|
||||||
|
).Scan(&hasPub); err == nil && hasPub {
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
if err := pool.QueryRow(ctx,
|
||||||
|
`SELECT EXISTS(SELECT 1 FROM pg_subscription WHERE subname = 'edgeguard_sub')`,
|
||||||
|
).Scan(&hasSub); err == nil && hasSub {
|
||||||
|
return "peer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Keine Replikation eingerichtet: ein per Join dazugekommener Node ist
|
||||||
|
// trotzdem kein Primary, alles andere (Founder/Single-Node) schon.
|
||||||
|
if st != nil && st.IsClusterNode {
|
||||||
|
return "peer"
|
||||||
|
}
|
||||||
|
return "primary"
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,8 +25,15 @@ type Store struct {
|
|||||||
|
|
||||||
func NewStore(pool *pgxpool.Pool) *Store { return &Store{Pool: pool} }
|
func NewStore(pool *pgxpool.Pool) *Store { return &Store{Pool: pool} }
|
||||||
|
|
||||||
|
// baseSelect MUSS spaltenweise zu scanNode passen. pg_role fehlte hier
|
||||||
|
// urspruenglich (Befund 2026-09-11): HANode.PGRole kam dadurch ueberall als
|
||||||
|
// leerer String an, wo Store.List/Get benutzt wird — /cluster/nodes,
|
||||||
|
// /cluster/status und damit auch die pg_role-Spalte der Cluster-Seite.
|
||||||
|
// UpsertSelf schreibt pg_role bewusst NICHT (ON CONFLICT laesst die Spalte
|
||||||
|
// unangetastet), liest sie aber im RETURNING mit — sonst passt die
|
||||||
|
// Scan-Reihenfolge nicht.
|
||||||
const baseSelect = `
|
const baseSelect = `
|
||||||
SELECT id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip, role,
|
SELECT id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip, role, pg_role,
|
||||||
version, config_hash, status,
|
version, config_hash, status,
|
||||||
last_seen, joined_at, created_at, updated_at
|
last_seen, joined_at, created_at, updated_at
|
||||||
FROM ha_nodes
|
FROM ha_nodes
|
||||||
@@ -90,7 +97,7 @@ ON CONFLICT (id) DO UPDATE SET
|
|||||||
last_seen = EXCLUDED.last_seen,
|
last_seen = EXCLUDED.last_seen,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
RETURNING id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip,
|
RETURNING id, name, fqdn, api_url, public_ip, internal_ip, mgmt_ip,
|
||||||
role, version, config_hash, status,
|
role, pg_role, version, config_hash, status,
|
||||||
last_seen, joined_at, created_at, updated_at`,
|
last_seen, joined_at, created_at, updated_at`,
|
||||||
n.ID, n.Name, n.FQDN, n.APIURL,
|
n.ID, n.Name, n.FQDN, n.APIURL,
|
||||||
n.PublicIP, n.InternalIP, n.MgmtIP,
|
n.PublicIP, n.InternalIP, n.MgmtIP,
|
||||||
@@ -181,7 +188,7 @@ func scanNode(row interface{ Scan(...any) error }) (*models.HANode, error) {
|
|||||||
if err := row.Scan(
|
if err := row.Scan(
|
||||||
&n.ID, &n.Name, &n.FQDN, &n.APIURL,
|
&n.ID, &n.Name, &n.FQDN, &n.APIURL,
|
||||||
&n.PublicIP, &n.InternalIP, &n.MgmtIP,
|
&n.PublicIP, &n.InternalIP, &n.MgmtIP,
|
||||||
&n.Role, &n.Version, &n.ConfigHash, &n.Status,
|
&n.Role, &n.PGRole, &n.Version, &n.ConfigHash, &n.Status,
|
||||||
&n.LastSeen, &n.JoinedAt,
|
&n.LastSeen, &n.JoinedAt,
|
||||||
&n.CreatedAt, &n.UpdatedAt,
|
&n.CreatedAt, &n.UpdatedAt,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user